From cf9f41cf613a57066f301fe3da651e6c7d5ea1b7 Mon Sep 17 00:00:00 2001 From: tdcosta100 Date: Tue, 7 Jan 2025 10:32:17 -0300 Subject: [PATCH] Add Initial PMTiles support (#2882) Co-authored-by: Bart Louwers --- .gitmodules | 3 + BUILD.bazel | 1 + CMakeLists.txt | 4 + bazel/core.bzl | 1 + include/mbgl/storage/file_source.hpp | 1 + include/mbgl/storage/resource.hpp | 1 + include/mbgl/util/constants.hpp | 1 + .../src/cpp/http_file_source.cpp | 9 +- .../maplibre/android/http/HttpRequest.java | 3 +- .../android/http/NativeHttpRequest.java | 5 +- .../android/module/http/HttpRequestImpl.java | 8 +- .../testapp/utils/ExampleHttpRequestImpl.kt | 4 +- platform/android/android.cmake | 1 + platform/darwin/src/http_file_source.mm | 9 +- platform/default/BUILD.bazel | 1 + .../mbgl/storage/local_file_request.hpp | 4 +- .../src/mbgl/storage/file_source_manager.cpp | 6 + .../src/mbgl/storage/http_file_source.cpp | 8 +- .../src/mbgl/storage/local_file_request.cpp | 6 +- .../src/mbgl/storage/local_file_source.cpp | 11 +- .../src/mbgl/storage/main_resource_loader.cpp | 26 +- .../src/mbgl/storage/pmtiles_file_source.cpp | 586 ++++++++++++++++++ .../mbgl/storage/pmtiles_file_source_stub.cpp | 38 ++ platform/linux/linux.cmake | 1 + platform/macos/macos.cmake | 1 + platform/qt/qt.cmake | 1 + platform/qt/src/mbgl/http_request.cpp | 9 +- platform/windows/windows.cmake | 1 + src/mbgl/storage/pmtiles_file_source.hpp | 29 + src/mbgl/util/io.cpp | 17 +- src/mbgl/util/io.hpp | 4 +- test/CMakeLists.txt | 1 + .../pmtiles/geography-class-png.pmtiles | Bin 0 -> 88874 bytes test/src/mbgl/test/http_server.cpp | 13 +- test/storage/http_file_source.test.cpp | 21 + test/storage/local_file_source.test.cpp | 22 +- test/storage/pmtiles_file_source.test.cpp | 105 ++++ test/storage/server.js | 8 +- vendor/BUILD.bazel | 9 + vendor/PMTiles | 1 + vendor/pmtiles.cmake | 21 + 41 files changed, 969 insertions(+), 32 deletions(-) create mode 100644 platform/default/src/mbgl/storage/pmtiles_file_source.cpp create mode 100644 platform/default/src/mbgl/storage/pmtiles_file_source_stub.cpp create mode 100644 src/mbgl/storage/pmtiles_file_source.hpp create mode 100644 test/fixtures/storage/pmtiles/geography-class-png.pmtiles create mode 100644 test/storage/pmtiles_file_source.test.cpp create mode 160000 vendor/PMTiles create mode 100644 vendor/pmtiles.cmake diff --git a/.gitmodules b/.gitmodules index 9876bf6711b..72537dd37ef 100644 --- a/.gitmodules +++ b/.gitmodules @@ -55,3 +55,6 @@ [submodule "vendor/glslang"] path = vendor/glslang url = https://github.com/KhronosGroup/glslang.git +[submodule "vendor/PMTiles"] + path = vendor/PMTiles + url = https://github.com/protomaps/PMTiles.git diff --git a/BUILD.bazel b/BUILD.bazel index 9e7d53e0373..80c7ccf3411 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -129,6 +129,7 @@ cc_library( "//vendor:eternal", "//vendor:mapbox-base", "//vendor:parsedate", + "//vendor:pmtiles", "//vendor:polylabel", "//vendor:protozero", "//vendor:unique_resource", diff --git a/CMakeLists.txt b/CMakeLists.txt index 52267c770b0..fc107b4b8c3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -11,6 +11,7 @@ option(MLN_WITH_OPENGL "Build with OpenGL renderer" ON) option(MLN_WITH_EGL "Build with EGL renderer" OFF) option(MLN_WITH_VULKAN "Build with Vulkan renderer" OFF) option(MLN_WITH_OSMESA "Build with OSMesa (Software) renderer" OFF) +option(MLN_WITH_PMTILES "Build with PMTiles support" ON) option(MLN_WITH_WERROR "Make all compilation warnings errors" ON) option(MLN_LEGACY_RENDERER "Include the legacy rendering pathway" ON) option(MLN_DRAWABLE_RENDERER "Include the drawable rendering pathway" OFF) @@ -1450,6 +1451,7 @@ include(${PROJECT_SOURCE_DIR}/vendor/earcut.hpp.cmake) include(${PROJECT_SOURCE_DIR}/vendor/eternal.cmake) include(${PROJECT_SOURCE_DIR}/vendor/mapbox-base.cmake) include(${PROJECT_SOURCE_DIR}/vendor/parsedate.cmake) +include(${PROJECT_SOURCE_DIR}/vendor/pmtiles.cmake) include(${PROJECT_SOURCE_DIR}/vendor/polylabel.cmake) include(${PROJECT_SOURCE_DIR}/vendor/protozero.cmake) include(${PROJECT_SOURCE_DIR}/vendor/tracy.cmake) @@ -1473,6 +1475,7 @@ target_link_libraries( mbgl-vendor-earcut.hpp mbgl-vendor-eternal mbgl-vendor-parsedate + mbgl-vendor-pmtiles mbgl-vendor-polylabel mbgl-vendor-protozero mbgl-vendor-unique_resource @@ -1512,6 +1515,7 @@ export(TARGETS mbgl-vendor-earcut.hpp mbgl-vendor-eternal mbgl-vendor-parsedate + mbgl-vendor-pmtiles mbgl-vendor-polylabel mbgl-vendor-protozero mbgl-vendor-unique_resource diff --git a/bazel/core.bzl b/bazel/core.bzl index 472fde18105..85c903f5b37 100644 --- a/bazel/core.bzl +++ b/bazel/core.bzl @@ -377,6 +377,7 @@ MLN_CORE_SOURCE = [ "src/mbgl/storage/local_file_source.hpp", "src/mbgl/storage/main_resource_loader.hpp", "src/mbgl/storage/network_status.cpp", + "src/mbgl/storage/pmtiles_file_source.hpp", "src/mbgl/storage/resource.cpp", "src/mbgl/storage/resource_options.cpp", "src/mbgl/storage/resource_transform.cpp", diff --git a/include/mbgl/storage/file_source.hpp b/include/mbgl/storage/file_source.hpp index 4c896c4d6fc..6a70154df41 100644 --- a/include/mbgl/storage/file_source.hpp +++ b/include/mbgl/storage/file_source.hpp @@ -25,6 +25,7 @@ enum FileSourceType : uint8_t { FileSystem, Network, Mbtiles, + Pmtiles, ResourceLoader ///< %Resource loader acts as a proxy and has logic /// for request delegation to Asset, Cache, and other /// file sources. diff --git a/include/mbgl/storage/resource.hpp b/include/mbgl/storage/resource.hpp index 114bd2f7640..26fda2752ba 100644 --- a/include/mbgl/storage/resource.hpp +++ b/include/mbgl/storage/resource.hpp @@ -95,6 +95,7 @@ class Resource { // Includes auxiliary data if this is a tile request. std::optional tileData; + std::optional> dataRange = std::nullopt; std::optional priorModified = std::nullopt; std::optional priorExpires = std::nullopt; std::optional priorEtag = std::nullopt; diff --git a/include/mbgl/util/constants.hpp b/include/mbgl/util/constants.hpp index 51900f93ed1..847c72a2548 100644 --- a/include/mbgl/util/constants.hpp +++ b/include/mbgl/util/constants.hpp @@ -60,6 +60,7 @@ constexpr int DEFAULT_RATE_LIMIT_TIMEOUT = 5; constexpr const char* ASSET_PROTOCOL = "asset://"; constexpr const char* FILE_PROTOCOL = "file://"; constexpr const char* MBTILES_PROTOCOL = "mbtiles://"; +constexpr const char* PMTILES_PROTOCOL = "pmtiles://"; constexpr uint32_t DEFAULT_MAXIMUM_CONCURRENT_REQUESTS = 20; constexpr uint8_t TERRAIN_RGB_MAXZOOM = 15; diff --git a/platform/android/MapLibreAndroid/src/cpp/http_file_source.cpp b/platform/android/MapLibreAndroid/src/cpp/http_file_source.cpp index 92420ca7d8e..4bd80f1e78f 100644 --- a/platform/android/MapLibreAndroid/src/cpp/http_file_source.cpp +++ b/platform/android/MapLibreAndroid/src/cpp/http_file_source.cpp @@ -91,9 +91,15 @@ void RegisterNativeHTTPRequest(jni::JNIEnv& env) { HTTPRequest::HTTPRequest(jni::JNIEnv& env, const Resource& resource_, FileSource::Callback callback_) : resource(resource_), callback(callback_) { + std::string dataRangeStr; std::string etagStr; std::string modifiedStr; + if (resource.dataRange) { + dataRangeStr = std::string("bytes=") + std::to_string(resource.dataRange->first) + std::string("-") + + std::to_string(resource.dataRange->second); + } + if (resource.priorEtag) { etagStr = *resource.priorEtag; } else if (resource.priorModified) { @@ -104,13 +110,14 @@ HTTPRequest::HTTPRequest(jni::JNIEnv& env, const Resource& resource_, FileSource static auto& javaClass = jni::Class::Singleton(env); static auto constructor = - javaClass.GetConstructor(env); + javaClass.GetConstructor(env); javaRequest = jni::NewGlobal(env, javaClass.New(env, constructor, reinterpret_cast(this), jni::Make(env, resource.url), + jni::Make(env, dataRangeStr), jni::Make(env, etagStr), jni::Make(env, modifiedStr), (jboolean)(resource_.usage == Resource::Usage::Offline))); diff --git a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/http/HttpRequest.java b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/http/HttpRequest.java index ffe8e21a8a2..05a889192e1 100644 --- a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/http/HttpRequest.java +++ b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/http/HttpRequest.java @@ -18,12 +18,13 @@ public interface HttpRequest { * @param httpRequest callback to be invoked when we receive a response * @param nativePtr the pointer associated to the request * @param resourceUrl the resource url to download + * @param dataRange http header, used to indicate the part of a resource that the server should return * @param etag http header, identifier for a specific version of a resource * @param modified http header, used to determine if a resource hasn't been modified since * @param offlineUsage flag to indicate a resource will be used for offline, appends offline=true as a query parameter */ void executeRequest(HttpResponder httpRequest, long nativePtr, String resourceUrl, - String etag, String modified, boolean offlineUsage); + String dataRange, String etag, String modified, boolean offlineUsage); /** * Cancels the request. diff --git a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/http/NativeHttpRequest.java b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/http/NativeHttpRequest.java index e41429cd3f5..0c4c43aab68 100644 --- a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/http/NativeHttpRequest.java +++ b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/http/NativeHttpRequest.java @@ -19,7 +19,8 @@ public class NativeHttpRequest implements HttpResponder { private long nativePtr; @Keep - private NativeHttpRequest(long nativePtr, String resourceUrl, String etag, String modified, boolean offlineUsage) { + private NativeHttpRequest(long nativePtr, String resourceUrl, String dataRange, String etag, String modified, + boolean offlineUsage) { this.nativePtr = nativePtr; if (resourceUrl.startsWith("local://")) { @@ -27,7 +28,7 @@ private NativeHttpRequest(long nativePtr, String resourceUrl, String etag, Strin executeLocalRequest(resourceUrl); return; } - httpRequest.executeRequest(this, nativePtr, resourceUrl, etag, modified, offlineUsage); + httpRequest.executeRequest(this, nativePtr, resourceUrl, dataRange, etag, modified, offlineUsage); } public void cancel() { diff --git a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/module/http/HttpRequestImpl.java b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/module/http/HttpRequestImpl.java index 275e363cd21..7b5ca63c76a 100644 --- a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/module/http/HttpRequestImpl.java +++ b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/module/http/HttpRequestImpl.java @@ -58,7 +58,8 @@ public class HttpRequestImpl implements HttpRequest { @Override public void executeRequest(HttpResponder httpRequest, long nativePtr, @NonNull String resourceUrl, - @NonNull String etag, @NonNull String modified, boolean offlineUsage) { + @NonNull String dataRange, @NonNull String etag, @NonNull String modified, + boolean offlineUsage) { OkHttpCallback callback = new OkHttpCallback(httpRequest); try { HttpUrl httpUrl = HttpUrl.parse(resourceUrl); @@ -74,6 +75,11 @@ public void executeRequest(HttpResponder httpRequest, long nativePtr, @NonNull S .url(resourceUrl) .tag(resourceUrl.toLowerCase(MapLibreConstants.MAPLIBRE_LOCALE)) .addHeader("User-Agent", userAgentString); + + if (dataRange.length() > 0) { + builder.addHeader("Range", dataRange); + } + if (etag.length() > 0) { builder.addHeader("If-None-Match", etag); } else if (modified.length() > 0) { diff --git a/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/utils/ExampleHttpRequestImpl.kt b/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/utils/ExampleHttpRequestImpl.kt index a9d2840696f..e49f567f411 100644 --- a/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/utils/ExampleHttpRequestImpl.kt +++ b/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/utils/ExampleHttpRequestImpl.kt @@ -11,11 +11,11 @@ import org.maplibre.android.module.http.HttpRequestImpl */ class ExampleHttpRequestImpl : HttpRequest { override fun executeRequest(httpRequest: HttpResponder, nativePtr: Long, resourceUrl: String, - etag: String, modified: String, offlineUsage: Boolean) + dataRange: String, etag: String, modified: String, offlineUsage: Boolean) { // Load all json documents and any pbf ending with a 0. if (resourceUrl.endsWith(".json") || resourceUrl.endsWith("0.pbf")) { - impl.executeRequest(httpRequest, nativePtr, resourceUrl, etag, modified, offlineUsage) + impl.executeRequest(httpRequest, nativePtr, resourceUrl, dataRange, etag, modified, offlineUsage) } else { // All other requests get an instant 404! httpRequest.onResponse( diff --git a/platform/android/android.cmake b/platform/android/android.cmake index dd01b8f11e1..fbddbb2a4f0 100644 --- a/platform/android/android.cmake +++ b/platform/android/android.cmake @@ -61,6 +61,7 @@ target_sources( ${PROJECT_SOURCE_DIR}/platform/default/src/mbgl/storage/offline_database.cpp ${PROJECT_SOURCE_DIR}/platform/default/src/mbgl/storage/offline_download.cpp ${PROJECT_SOURCE_DIR}/platform/default/src/mbgl/storage/online_file_source.cpp + ${PROJECT_SOURCE_DIR}/platform/default/src/mbgl/storage/$,pmtiles_file_source.cpp,pmtiles_file_source_stub.cpp> ${PROJECT_SOURCE_DIR}/platform/default/src/mbgl/storage/sqlite3.cpp ${PROJECT_SOURCE_DIR}/platform/default/src/mbgl/text/bidi.cpp ${PROJECT_SOURCE_DIR}/platform/default/src/mbgl/util/compression.cpp diff --git a/platform/darwin/src/http_file_source.mm b/platform/darwin/src/http_file_source.mm index 007a0b902d4..8c2fe83c4c3 100644 --- a/platform/darwin/src/http_file_source.mm +++ b/platform/darwin/src/http_file_source.mm @@ -264,6 +264,13 @@ BOOL isValidMapboxEndpoint(NSURL *url) { [req addValue:@(util::rfc1123(*resource.priorModified).c_str()) forHTTPHeaderField:@"If-Modified-Since"]; } + + if (resource.dataRange) { + NSString *rangeHeader = [NSString stringWithFormat:@"bytes=%lld-%lld", + static_cast(resource.dataRange->first), + static_cast(resource.dataRange->second)]; + [req setValue:rangeHeader forHTTPHeaderField:@"Range"]; + } [req addValue:impl->userAgent forHTTPHeaderField:@"User-Agent"]; @@ -360,7 +367,7 @@ BOOL isValidMapboxEndpoint(NSURL *url) { response.etag = std::string([etag UTF8String]); } - if (responseCode == 200) { + if (responseCode == 200 || responseCode == 206) { response.data = std::make_shared((const char *)[data bytes], [data length]); } else if (responseCode == 204 || (responseCode == 404 && isTile)) { response.noContent = true; diff --git a/platform/default/BUILD.bazel b/platform/default/BUILD.bazel index bb1dfd3576f..95f94e32ce6 100644 --- a/platform/default/BUILD.bazel +++ b/platform/default/BUILD.bazel @@ -57,6 +57,7 @@ cc_library( "src/mbgl/storage/offline_database.cpp", "src/mbgl/storage/offline_download.cpp", "src/mbgl/storage/online_file_source.cpp", + "src/mbgl/storage/pmtiles_file_source.cpp", "src/mbgl/storage/sqlite3.cpp", "src/mbgl/text/bidi.cpp", "src/mbgl/util/compression.cpp", diff --git a/platform/default/include/mbgl/storage/local_file_request.hpp b/platform/default/include/mbgl/storage/local_file_request.hpp index 15354d36a4e..0d342695422 100644 --- a/platform/default/include/mbgl/storage/local_file_request.hpp +++ b/platform/default/include/mbgl/storage/local_file_request.hpp @@ -8,6 +8,8 @@ template class ActorRef; class FileSourceRequest; -void requestLocalFile(const std::string&, const ActorRef&); +void requestLocalFile(const std::string& path, + const ActorRef& req, + const std::optional>& dataRange = std::nullopt); } // namespace mbgl diff --git a/platform/default/src/mbgl/storage/file_source_manager.cpp b/platform/default/src/mbgl/storage/file_source_manager.cpp index d3068404407..b78a94e6dc2 100644 --- a/platform/default/src/mbgl/storage/file_source_manager.cpp +++ b/platform/default/src/mbgl/storage/file_source_manager.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include namespace mbgl { @@ -37,6 +38,11 @@ class DefaultFileSourceManagerImpl final : public FileSourceManager { return std::make_unique(resourceOptions, clientOptions); }); + registerFileSourceFactory(FileSourceType::Pmtiles, + [](const ResourceOptions& resourceOptions, const ClientOptions& clientOptions) { + return std::make_unique(resourceOptions, clientOptions); + }); + registerFileSourceFactory(FileSourceType::Network, [](const ResourceOptions& resourceOptions, const ClientOptions& clientOptions) { return std::make_unique(resourceOptions, clientOptions); diff --git a/platform/default/src/mbgl/storage/http_file_source.cpp b/platform/default/src/mbgl/storage/http_file_source.cpp index 0b0d191c2d5..6bb4ff9220a 100644 --- a/platform/default/src/mbgl/storage/http_file_source.cpp +++ b/platform/default/src/mbgl/storage/http_file_source.cpp @@ -269,6 +269,12 @@ HTTPRequest::HTTPRequest(HTTPFileSource::Impl *context_, Resource resource_, Fil resource(std::move(resource_)), callback(std::move(callback_)), handle(context->getHandle()) { + if (resource.dataRange) { + const std::string header = std::string("Range: bytes=") + std::to_string(resource.dataRange->first) + + std::string("-") + std::to_string(resource.dataRange->second); + headers = curl_slist_append(headers, header.c_str()); + } + // If there's already a response, set the correct etags/modified headers to // make sure we are getting a 304 response if possible. This avoids // redownloading unchanged data. @@ -417,7 +423,7 @@ void HTTPRequest::handleResult(CURLcode code) { long responseCode = 0; curl_easy_getinfo(handle, CURLINFO_RESPONSE_CODE, &responseCode); - if (responseCode == 200) { + if (responseCode == 200 || responseCode == 206) { if (data) { response->data = std::move(data); } else { diff --git a/platform/default/src/mbgl/storage/local_file_request.cpp b/platform/default/src/mbgl/storage/local_file_request.cpp index 6204f552ae4..8c6cf43d808 100644 --- a/platform/default/src/mbgl/storage/local_file_request.cpp +++ b/platform/default/src/mbgl/storage/local_file_request.cpp @@ -11,7 +11,9 @@ namespace mbgl { -void requestLocalFile(const std::string& path, const ActorRef& req) { +void requestLocalFile(const std::string& path, + const ActorRef& req, + const std::optional>& dataRange) { Response response; struct stat buf; int result = stat(path.c_str(), &buf); @@ -21,7 +23,7 @@ void requestLocalFile(const std::string& path, const ActorRef } else if (result == -1 && errno == ENOENT) { response.error = std::make_unique(Response::Error::Reason::NotFound); } else { - auto data = util::readFile(path); + auto data = util::readFile(path, dataRange); if (!data) { response.error = std::make_unique(Response::Error::Reason::Other, std::string("Cannot read file ") + path); diff --git a/platform/default/src/mbgl/storage/local_file_source.cpp b/platform/default/src/mbgl/storage/local_file_source.cpp index c42ad520c1a..3fef8ee7862 100644 --- a/platform/default/src/mbgl/storage/local_file_source.cpp +++ b/platform/default/src/mbgl/storage/local_file_source.cpp @@ -25,8 +25,8 @@ class LocalFileSource::Impl { : resourceOptions(resourceOptions_.clone()), clientOptions(clientOptions_.clone()) {} - void request(const std::string& url, const ActorRef& req) { - if (!acceptsURL(url)) { + void request(const Resource& resource, const ActorRef& req) { + if (!acceptsURL(resource.url)) { Response response; response.error = std::make_unique(Response::Error::Reason::Other, "Invalid file URL"); req.invoke(&FileSourceRequest::setResponse, response); @@ -34,8 +34,9 @@ class LocalFileSource::Impl { } // Cut off the protocol and prefix with path. - const auto path = mbgl::util::percentDecode(url.substr(std::char_traits::length(util::FILE_PROTOCOL))); - requestLocalFile(path, req); + const auto path = mbgl::util::percentDecode( + resource.url.substr(std::char_traits::length(util::FILE_PROTOCOL))); + requestLocalFile(path, req, resource.dataRange); } void setResourceOptions(ResourceOptions options) { @@ -77,7 +78,7 @@ LocalFileSource::~LocalFileSource() = default; std::unique_ptr LocalFileSource::request(const Resource& resource, Callback callback) { auto req = std::make_unique(std::move(callback)); - impl->actor().invoke(&Impl::request, resource.url, req->actor()); + impl->actor().invoke(&Impl::request, resource, req->actor()); return req; } diff --git a/platform/default/src/mbgl/storage/main_resource_loader.cpp b/platform/default/src/mbgl/storage/main_resource_loader.cpp index 66699128fe9..2f523f1e8d2 100644 --- a/platform/default/src/mbgl/storage/main_resource_loader.cpp +++ b/platform/default/src/mbgl/storage/main_resource_loader.cpp @@ -21,12 +21,14 @@ class MainResourceLoaderThread { std::shared_ptr databaseFileSource_, std::shared_ptr localFileSource_, std::shared_ptr onlineFileSource_, - std::shared_ptr mbtilesFileSource_) + std::shared_ptr mbtilesFileSource_, + std::shared_ptr pmtilesFileSource_) : assetFileSource(std::move(assetFileSource_)), databaseFileSource(std::move(databaseFileSource_)), localFileSource(std::move(localFileSource_)), onlineFileSource(std::move(onlineFileSource_)), - mbtilesFileSource(std::move(mbtilesFileSource_)) {} + mbtilesFileSource(std::move(mbtilesFileSource_)), + pmtilesFileSource(std::move(pmtilesFileSource_)) {} void request(AsyncRequest* req, const Resource& resource, const ActorRef& ref) { auto callback = [ref](const Response& res) { @@ -70,6 +72,9 @@ class MainResourceLoaderThread { } else if (mbtilesFileSource && mbtilesFileSource->canRequest(resource)) { // Local file request tasks[req] = mbtilesFileSource->request(resource, callback); + } else if (pmtilesFileSource && pmtilesFileSource->canRequest(resource)) { + // Local file request + tasks[req] = pmtilesFileSource->request(resource, callback); } else if (localFileSource && localFileSource->canRequest(resource)) { // Local file request tasks[req] = localFileSource->request(resource, callback); @@ -131,6 +136,7 @@ class MainResourceLoaderThread { const std::shared_ptr localFileSource; const std::shared_ptr onlineFileSource; const std::shared_ptr mbtilesFileSource; + const std::shared_ptr pmtilesFileSource; std::map> tasks; }; @@ -142,12 +148,14 @@ class MainResourceLoader::Impl { std::shared_ptr databaseFileSource_, std::shared_ptr localFileSource_, std::shared_ptr onlineFileSource_, - std::shared_ptr mbtilesFileSource_) + std::shared_ptr mbtilesFileSource_, + std::shared_ptr pmtilesFileSource_) : assetFileSource(std::move(assetFileSource_)), databaseFileSource(std::move(databaseFileSource_)), localFileSource(std::move(localFileSource_)), onlineFileSource(std::move(onlineFileSource_)), mbtilesFileSource(std::move(mbtilesFileSource_)), + pmtilesFileSource(std::move(pmtilesFileSource_)), supportsCacheOnlyRequests_(bool(databaseFileSource)), thread(std::make_unique>( util::makeThreadPrioritySetter(platform::EXPERIMENTAL_THREAD_PRIORITY_WORKER), @@ -156,7 +164,8 @@ class MainResourceLoader::Impl { databaseFileSource, localFileSource, onlineFileSource, - mbtilesFileSource)), + mbtilesFileSource, + pmtilesFileSource)), resourceOptions(resourceOptions_.clone()), clientOptions(clientOptions_.clone()) {} @@ -175,7 +184,8 @@ class MainResourceLoader::Impl { (localFileSource && localFileSource->canRequest(resource)) || (databaseFileSource && databaseFileSource->canRequest(resource)) || (onlineFileSource && onlineFileSource->canRequest(resource)) || - (mbtilesFileSource && mbtilesFileSource->canRequest(resource)); + (mbtilesFileSource && mbtilesFileSource->canRequest(resource)) || + (pmtilesFileSource && pmtilesFileSource->canRequest(resource)); } bool supportsCacheOnlyRequests() const { return supportsCacheOnlyRequests_; } @@ -192,6 +202,7 @@ class MainResourceLoader::Impl { localFileSource->setResourceOptions(options.clone()); onlineFileSource->setResourceOptions(options.clone()); mbtilesFileSource->setResourceOptions(options.clone()); + pmtilesFileSource->setResourceOptions(options.clone()); } ResourceOptions getResourceOptions() { @@ -207,6 +218,7 @@ class MainResourceLoader::Impl { localFileSource->setClientOptions(options.clone()); onlineFileSource->setClientOptions(options.clone()); mbtilesFileSource->setClientOptions(options.clone()); + pmtilesFileSource->setClientOptions(options.clone()); } ClientOptions getClientOptions() { @@ -220,6 +232,7 @@ class MainResourceLoader::Impl { const std::shared_ptr localFileSource; const std::shared_ptr onlineFileSource; const std::shared_ptr mbtilesFileSource; + const std::shared_ptr pmtilesFileSource; const bool supportsCacheOnlyRequests_; const std::unique_ptr> thread; mutable std::mutex resourceOptionsMutex; @@ -236,7 +249,8 @@ MainResourceLoader::MainResourceLoader(const ResourceOptions& resourceOptions, c FileSourceManager::get()->getFileSource(FileSourceType::Database, resourceOptions, clientOptions), FileSourceManager::get()->getFileSource(FileSourceType::FileSystem, resourceOptions, clientOptions), FileSourceManager::get()->getFileSource(FileSourceType::Network, resourceOptions, clientOptions), - FileSourceManager::get()->getFileSource(FileSourceType::Mbtiles, resourceOptions, clientOptions))) {} + FileSourceManager::get()->getFileSource(FileSourceType::Mbtiles, resourceOptions, clientOptions), + FileSourceManager::get()->getFileSource(FileSourceType::Pmtiles, resourceOptions, clientOptions))) {} MainResourceLoader::~MainResourceLoader() = default; diff --git a/platform/default/src/mbgl/storage/pmtiles_file_source.cpp b/platform/default/src/mbgl/storage/pmtiles_file_source.cpp new file mode 100644 index 00000000000..9089f65ea8f --- /dev/null +++ b/platform/default/src/mbgl/storage/pmtiles_file_source.cpp @@ -0,0 +1,586 @@ +#include +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include + +#include +#include + +#if defined(__QT__) && (defined(_WIN32) || defined(__EMSCRIPTEN__)) +#include +#else +#include +#endif + +namespace { +// https://github.com/protomaps/PMTiles/blob/main/spec/v3/spec.md#3-header +constexpr int pmtilesHeaderOffset = 0; +constexpr int pmtilesHeaderLength = 127; + +// To avoid allocating lots of memory with PMTiles directory caching, +// set a limit so it doesn't grow unlimited +constexpr int MAX_DIRECTORY_CACHE_ENTRIES = 100; + +bool acceptsURL(const std::string& url) { + return url.starts_with(mbgl::util::PMTILES_PROTOCOL); +} + +std::string extract_url(const std::string& url) { + return url.substr(std::char_traits::length(mbgl::util::PMTILES_PROTOCOL)); +} +} // namespace + +namespace mbgl { +using namespace rapidjson; + +using AsyncCallback = std::function)>; +using AsyncTileCallback = std::function, std::unique_ptr)>; + +class PMTilesFileSource::Impl { +public: + explicit Impl(const ActorRef&, const ResourceOptions& resourceOptions_, const ClientOptions& clientOptions_) + : resourceOptions(resourceOptions_.clone()), + clientOptions(clientOptions_.clone()) {} + + // Generate a tilejson resource from .pmtiles file + void request_tilejson(AsyncRequest* req, const Resource& resource, const ActorRef& ref) { + auto url = extract_url(resource.url); + + getMetadata(url, req, [=, this](std::unique_ptr error) { + Response response; + + if (error) { + response.error = std::move(error); + ref.invoke(&FileSourceRequest::setResponse, response); + return; + } + + response.data = std::make_shared(metadata_cache.at(url)); + ref.invoke(&FileSourceRequest::setResponse, response); + }); + } + + // Load data for specific tile + void request_tile(AsyncRequest* req, const Resource& resource, ActorRef ref) { + auto url = extract_url(resource.url); + + getHeader(url, req, [=, this](std::unique_ptr error) { + if (error) { + Response response; + response.noContent = true; + response.error = std::move(error); + ref.invoke(&FileSourceRequest::setResponse, response); + return; + } + + pmtiles::headerv3 header = header_cache.at(url); + + if (resource.tileData->z < header.min_zoom || resource.tileData->z > header.max_zoom) { + Response response; + response.noContent = true; + ref.invoke(&FileSourceRequest::setResponse, response); + return; + } + + uint64_t tileID; + + try { + tileID = pmtiles::zxy_to_tileid(static_cast(resource.tileData->z), + static_cast(resource.tileData->x), + static_cast(resource.tileData->y)); + } catch (const std::exception& e) { + Response response; + response.noContent = true; + response.error = std::make_unique(Response::Error::Reason::Other, + std::string("Invalid tile: ") + e.what()); + ref.invoke(&FileSourceRequest::setResponse, response); + return; + } + + getTileAddress( + url, + req, + tileID, + header.root_dir_offset, + header.root_dir_bytes, + 0, + [=, this](std::pair tileAddress, std::unique_ptr tileError) { + if (tileError) { + Response response; + response.noContent = true; + response.error = std::move(tileError); + ref.invoke(&FileSourceRequest::setResponse, response); + return; + } + + if (tileAddress.first == 0 && tileAddress.second == 0) { + Response response; + response.noContent = true; + ref.invoke(&FileSourceRequest::setResponse, response); + return; + } + + Resource tileResource(Resource::Kind::Source, url); + tileResource.loadingMethod = Resource::LoadingMethod::Network; + tileResource.dataRange = std::make_pair(tileAddress.first, + tileAddress.first + tileAddress.second - 1); + + tasks[req] = getFileSource()->request(tileResource, [=](const Response& tileResponse) { + Response response; + response.noContent = true; + + if (tileResponse.error) { + response.error = std::make_unique( + tileResponse.error->reason, + std::string("Error fetching PMTiles tile: ") + tileResponse.error->message); + ref.invoke(&FileSourceRequest::setResponse, response); + return; + } + + response.data = tileResponse.data; + response.noContent = false; + response.modified = tileResponse.modified; + response.expires = tileResponse.expires; + response.etag = tileResponse.etag; + + if (header.tile_compression == pmtiles::COMPRESSION_GZIP) { + response.data = std::make_shared(util::decompress(*tileResponse.data)); + } + + ref.invoke(&FileSourceRequest::setResponse, response); + return; + }); + }); + }); + } + + void setResourceOptions(ResourceOptions options) { + std::lock_guard lock(resourceOptionsMutex); + resourceOptions = options; + } + + ResourceOptions getResourceOptions() { + std::lock_guard lock(resourceOptionsMutex); + return resourceOptions.clone(); + } + + void setClientOptions(ClientOptions options) { + std::lock_guard lock(clientOptionsMutex); + clientOptions = options; + } + + ClientOptions getClientOptions() { + std::lock_guard lock(clientOptionsMutex); + return clientOptions.clone(); + } + +private: + mutable std::mutex resourceOptionsMutex; + mutable std::mutex clientOptionsMutex; + ResourceOptions resourceOptions; + ClientOptions clientOptions; + + std::shared_ptr fileSource; + std::map header_cache; + std::map metadata_cache; + std::map>> directory_cache; + std::map> directory_cache_control; + std::map> tasks; + + std::shared_ptr getFileSource() { + if (!fileSource) { + fileSource = FileSourceManager::get()->getFileSource( + FileSourceType::ResourceLoader, resourceOptions, clientOptions); + } + + return fileSource; + } + + void getHeader(const std::string& url, AsyncRequest* req, AsyncCallback callback) { + if (header_cache.find(url) != header_cache.end()) { + callback(std::unique_ptr()); + } + + Resource resource(Resource::Kind::Source, url); + resource.loadingMethod = Resource::LoadingMethod::Network; + + resource.dataRange = std::make_pair(pmtilesHeaderOffset, + pmtilesHeaderOffset + pmtilesHeaderLength - 1); + + tasks[req] = getFileSource()->request(resource, [=, this](const Response& response) { + if (response.error) { + std::string message = std::string("Error fetching PMTiles header: ") + response.error->message; + + if (response.error->message.empty() && response.error->reason == Response::Error::Reason::NotFound) { + if (url.starts_with(mbgl::util::FILE_PROTOCOL)) { + message += "path not found: " + + url.substr(std::char_traits::length(mbgl::util::FILE_PROTOCOL)); + } else { + message += "url not found: " + url; + } + } + + callback(std::make_unique(response.error->reason, message)); + + return; + } + + try { + pmtiles::headerv3 header = pmtiles::deserialize_header(response.data->substr(0, 127)); + + if ((header.internal_compression != pmtiles::COMPRESSION_NONE && + header.internal_compression != pmtiles::COMPRESSION_GZIP) || + (header.tile_compression != pmtiles::COMPRESSION_NONE && + header.tile_compression != pmtiles::COMPRESSION_GZIP)) { + throw std::runtime_error("Compression method not supported"); + } + + header_cache.emplace(url, header); + + callback(std::unique_ptr()); + } catch (const std::exception& e) { + callback(std::make_unique(Response::Error::Reason::Other, + std::string("Error parsing PMTiles header: ") + e.what())); + } + }); + } + + void getMetadata(std::string& url, AsyncRequest* req, AsyncCallback callback) { + if (metadata_cache.find(url) != metadata_cache.end()) { + callback(std::unique_ptr()); + } + + getHeader(url, req, [=, this](std::unique_ptr error) { + if (error) { + callback(std::move(error)); + return; + } + + pmtiles::headerv3 header = header_cache.at(url); + + auto parse_callback = [=, this](const std::string& data) { + Document doc; + + auto& allocator = doc.GetAllocator(); + + if (!data.empty()) { + doc.Parse(data); + } + + if (!doc.IsObject()) { + doc.SetObject(); + } + + doc.AddMember("tilejson", "3.0.0", allocator); + + if (!doc.HasMember("scheme")) { + doc.AddMember("scheme", rapidjson::Value().SetString("xyz"), allocator); + } + + if (!doc.HasMember("tiles")) { + doc.AddMember("tiles", rapidjson::Value(), allocator); + } + + if (!doc["tiles"].IsArray()) { + doc["tiles"] = rapidjson::Value().SetArray().PushBack( + rapidjson::Value().SetString(std::string(util::PMTILES_PROTOCOL + url), allocator), allocator); + } + + if (!doc.HasMember("bounds")) { + doc.AddMember("bounds", rapidjson::Value(), allocator); + } + + if (!doc["bounds"].IsArray()) { + doc["bounds"] = rapidjson::Value() + .SetArray() + .PushBack(static_cast(header.min_lon_e7) / 1e7, allocator) + .PushBack(static_cast(header.min_lat_e7) / 1e7, allocator) + .PushBack(static_cast(header.max_lon_e7) / 1e7, allocator) + .PushBack(static_cast(header.max_lat_e7) / 1e7, allocator); + } + + if (!doc.HasMember("center")) { + doc.AddMember("center", rapidjson::Value(), allocator); + } + + if (!doc["center"].IsArray()) { + doc["center"] = rapidjson::Value() + .SetArray() + .PushBack(static_cast(header.center_lon_e7) / 1e7, allocator) + .PushBack(static_cast(header.center_lat_e7) / 1e7, allocator) + .PushBack(header.center_zoom, allocator); + } + + if (!doc.HasMember("minzoom")) { + doc.AddMember("minzoom", rapidjson::Value(), allocator); + } + + auto& minzoom = doc["minzoom"]; + + if (minzoom.IsString()) { + minzoom.SetInt(std::atoi(minzoom.GetString())); + } + + if (!minzoom.IsNumber()) { + minzoom = rapidjson::Value().SetUint(header.min_zoom); + } + + doc["minzoom"] = minzoom; + + if (!doc.HasMember("maxzoom")) { + doc.AddMember("maxzoom", rapidjson::Value(), allocator); + } + + auto& maxzoom = doc["maxzoom"]; + + if (maxzoom.IsString()) { + maxzoom.SetInt(std::atoi(maxzoom.GetString())); + } + + if (!maxzoom.IsNumber()) { + maxzoom = rapidjson::Value().SetUint(header.max_zoom); + } + + doc["maxzoom"] = maxzoom; + + std::string metadata = serialize(doc); + metadata_cache.emplace(url, metadata); + + callback(std::unique_ptr()); + }; + + if (header.json_metadata_bytes > 0) { + Resource resource(Resource::Kind::Source, url); + resource.loadingMethod = Resource::LoadingMethod::Network; + resource.dataRange = std::make_pair(header.json_metadata_offset, + header.json_metadata_offset + header.json_metadata_bytes - 1); + + tasks[req] = getFileSource()->request(resource, [=](const Response& responseMetadata) { + if (responseMetadata.error) { + callback(std::make_unique( + responseMetadata.error->reason, + std::string("Error fetching PMTiles metadata: ") + responseMetadata.error->message)); + + return; + } + + std::string data = *responseMetadata.data; + + if (header.internal_compression == pmtiles::COMPRESSION_GZIP) { + data = util::decompress(data); + } + + parse_callback(data); + }); + + return; + } + + parse_callback(std::string()); + }); + } + + void storeDirectory(const std::string& url, + uint64_t directoryOffset, + uint64_t directoryLength, + const std::string& directoryData) { + if (directory_cache.find(url) == directory_cache.end()) { + directory_cache.emplace(url, std::map>()); + directory_cache_control.emplace(url, std::vector()); + } + + std::string directory_cache_key = url + "|" + std::to_string(directoryOffset) + "|" + + std::to_string(directoryLength); + directory_cache.at(url).emplace(directory_cache_key, pmtiles::deserialize_directory(directoryData)); + directory_cache_control.at(url).emplace_back(directory_cache_key); + + if (directory_cache_control.at(url).size() > MAX_DIRECTORY_CACHE_ENTRIES) { + directory_cache.at(url).erase(directory_cache_control.at(url).front()); + directory_cache_control.at(url).erase(directory_cache_control.at(url).begin()); + } + } + + void getDirectory(const std::string& url, + AsyncRequest* req, + uint64_t directoryOffset, + uint32_t directoryLength, + AsyncCallback callback) { + std::string directory_cache_key = url + "|" + std::to_string(directoryOffset) + "|" + + std::to_string(directoryLength); + + if (directory_cache.find(url) != directory_cache.end() && + directory_cache.at(url).find(directory_cache_key) != directory_cache.at(url).end()) { + if (directory_cache_control.at(url).back() != directory_cache_key) { + directory_cache_control.at(url).emplace_back(directory_cache_key); + + for (auto it = directory_cache_control.at(url).begin(); it != directory_cache_control.at(url).end(); + ++it) { + if (*it == directory_cache_key) { + directory_cache_control.at(url).erase(it); + break; + } + } + } + + callback(std::unique_ptr()); + return; + } + + getHeader(url, req, [=, this](std::unique_ptr error) { + if (error) { + callback(std::move(error)); + return; + } + + pmtiles::headerv3 header = header_cache.at(url); + + Resource resource(Resource::Kind::Source, url); + resource.loadingMethod = Resource::LoadingMethod::Network; + resource.dataRange = std::make_pair(directoryOffset, directoryOffset + directoryLength - 1); + + tasks[req] = getFileSource()->request(resource, [=, this](const Response& response) { + if (response.error) { + callback(std::make_unique( + response.error->reason, + std::string("Error fetching PMTiles directory: ") + response.error->message)); + + return; + } + + try { + std::string directoryData = *response.data; + + if (header.internal_compression == pmtiles::COMPRESSION_GZIP) { + directoryData = util::decompress(directoryData); + } + + storeDirectory(url, directoryOffset, directoryLength, directoryData); + + callback(std::unique_ptr()); + } catch (const std::exception& e) { + callback(std::make_unique( + Response::Error::Reason::Other, + std::string(std::string("Error parsing PMTiles directory: ") + e.what()))); + } + }); + }); + } + + void getTileAddress(const std::string& url, + AsyncRequest* req, + uint64_t tileID, + uint64_t directoryOffset, + uint32_t directoryLength, + uint32_t directoryDepth, + AsyncTileCallback callback) { + if (directoryDepth > 3) { + callback(std::make_pair(0, 0), + std::make_unique( + Response::Error::Reason::Other, + std::string("Error fetching PMTiles tile address: Maximum directory depth exceeded"))); + + return; + } + + getDirectory(url, req, directoryOffset, directoryLength, [=, this](std::unique_ptr error) { + if (error) { + callback(std::make_pair(0, 0), std::move(error)); + return; + } + + pmtiles::headerv3 header = header_cache.at(url); + std::vector directory = directory_cache.at(url).at( + url + "|" + std::to_string(directoryOffset) + "|" + std::to_string(directoryLength)); + + pmtiles::entryv3 entry = pmtiles::find_tile(directory, tileID); + + if (entry.length > 0) { + if (entry.run_length > 0) { + callback(std::make_pair(header.tile_data_offset + entry.offset, entry.length), {}); + return; + } + + getTileAddress(url, + req, + tileID, + header.leaf_dirs_offset + entry.offset, + entry.length, + directoryDepth + 1, + std::move(callback)); + return; + } + + callback(std::make_pair(0, 0), {}); + }); + } + + std::string serialize(Document& doc) { + StringBuffer buffer; + Writer writer(buffer); + doc.Accept(writer); + + return std::string(buffer.GetString(), buffer.GetSize()); + } +}; + +PMTilesFileSource::PMTilesFileSource(const ResourceOptions& resourceOptions, const ClientOptions& clientOptions) + : thread(std::make_unique>( + util::makeThreadPrioritySetter(platform::EXPERIMENTAL_THREAD_PRIORITY_FILE), + "PMTilesFileSource", + resourceOptions.clone(), + clientOptions.clone())) {} + +std::unique_ptr PMTilesFileSource::request(const Resource& resource, FileSource::Callback callback) { + auto req = std::make_unique(std::move(callback)); + + // assume if there is a tile request, that the pmtiles file has been validated + if (resource.kind == Resource::Tile) { + thread->actor().invoke(&Impl::request_tile, req.get(), resource, req->actor()); + return req; + } + + // return TileJSON + thread->actor().invoke(&Impl::request_tilejson, req.get(), resource, req->actor()); + return req; +} + +bool PMTilesFileSource::canRequest(const Resource& resource) const { + return acceptsURL(resource.url); +} + +PMTilesFileSource::~PMTilesFileSource() = default; + +void PMTilesFileSource::setResourceOptions(ResourceOptions options) { + thread->actor().invoke(&Impl::setResourceOptions, options.clone()); +} + +ResourceOptions PMTilesFileSource::getResourceOptions() { + return thread->actor().ask(&Impl::getResourceOptions).get(); +} + +void PMTilesFileSource::setClientOptions(ClientOptions options) { + thread->actor().invoke(&Impl::setClientOptions, options.clone()); +} + +ClientOptions PMTilesFileSource::getClientOptions() { + return thread->actor().ask(&Impl::getClientOptions).get(); +} + +} // namespace mbgl diff --git a/platform/default/src/mbgl/storage/pmtiles_file_source_stub.cpp b/platform/default/src/mbgl/storage/pmtiles_file_source_stub.cpp new file mode 100644 index 00000000000..48ff404756a --- /dev/null +++ b/platform/default/src/mbgl/storage/pmtiles_file_source_stub.cpp @@ -0,0 +1,38 @@ +#include +#include +#include +#include + +namespace mbgl { + +class PMTilesFileSource::Impl { +public: + Impl() = default; + ~Impl() = default; +}; + +PMTilesFileSource::PMTilesFileSource(const ResourceOptions& resourceOptions, const ClientOptions& clientOptions) {} + +std::unique_ptr PMTilesFileSource::request(const Resource& resource, FileSource::Callback callback) { + return nullptr; +} + +bool PMTilesFileSource::canRequest(const Resource& resource) const { + return false; +} + +PMTilesFileSource::~PMTilesFileSource() = default; + +void PMTilesFileSource::setResourceOptions(ResourceOptions options) {} + +ResourceOptions PMTilesFileSource::getResourceOptions() { + return {}; +} + +void PMTilesFileSource::setClientOptions(ClientOptions options) {} + +ClientOptions PMTilesFileSource::getClientOptions() { + return {}; +} + +} // namespace mbgl diff --git a/platform/linux/linux.cmake b/platform/linux/linux.cmake index e1374d067a5..4289fd57a63 100644 --- a/platform/linux/linux.cmake +++ b/platform/linux/linux.cmake @@ -50,6 +50,7 @@ target_sources( ${PROJECT_SOURCE_DIR}/platform/default/src/mbgl/storage/offline_database.cpp ${PROJECT_SOURCE_DIR}/platform/default/src/mbgl/storage/offline_download.cpp ${PROJECT_SOURCE_DIR}/platform/default/src/mbgl/storage/online_file_source.cpp + ${PROJECT_SOURCE_DIR}/platform/default/src/mbgl/storage/$,pmtiles_file_source.cpp,pmtiles_file_source_stub.cpp> ${PROJECT_SOURCE_DIR}/platform/default/src/mbgl/storage/sqlite3.cpp ${PROJECT_SOURCE_DIR}/platform/default/src/mbgl/text/bidi.cpp ${PROJECT_SOURCE_DIR}/platform/default/src/mbgl/text/local_glyph_rasterizer.cpp diff --git a/platform/macos/macos.cmake b/platform/macos/macos.cmake index 4ca02db212c..48953eb5b74 100644 --- a/platform/macos/macos.cmake +++ b/platform/macos/macos.cmake @@ -92,6 +92,7 @@ target_sources( ${PROJECT_SOURCE_DIR}/platform/default/src/mbgl/storage/offline_database.cpp ${PROJECT_SOURCE_DIR}/platform/default/src/mbgl/storage/offline_download.cpp ${PROJECT_SOURCE_DIR}/platform/default/src/mbgl/storage/online_file_source.cpp + ${PROJECT_SOURCE_DIR}/platform/default/src/mbgl/storage/$,pmtiles_file_source.cpp,pmtiles_file_source_stub.cpp> ${PROJECT_SOURCE_DIR}/platform/default/src/mbgl/storage/sqlite3.cpp ${PROJECT_SOURCE_DIR}/platform/default/src/mbgl/text/bidi.cpp ${PROJECT_SOURCE_DIR}/platform/default/src/mbgl/util/compression.cpp diff --git a/platform/qt/qt.cmake b/platform/qt/qt.cmake index 0f2bf061b35..04920ca8b38 100644 --- a/platform/qt/qt.cmake +++ b/platform/qt/qt.cmake @@ -84,6 +84,7 @@ target_sources( ${PROJECT_SOURCE_DIR}/platform/default/src/mbgl/storage/offline_database.cpp ${PROJECT_SOURCE_DIR}/platform/default/src/mbgl/storage/offline_download.cpp ${PROJECT_SOURCE_DIR}/platform/default/src/mbgl/storage/online_file_source.cpp + ${PROJECT_SOURCE_DIR}/platform/default/src/mbgl/storage/$,pmtiles_file_source.cpp,pmtiles_file_source_stub.cpp> ${PROJECT_SOURCE_DIR}/platform/$,default/src/mbgl/storage/sqlite3.cpp,qt/src/mbgl/sqlite3.cpp> ${PROJECT_SOURCE_DIR}/platform/default/src/mbgl/util/compression.cpp ${PROJECT_SOURCE_DIR}/platform/default/src/mbgl/util/filesystem.cpp diff --git a/platform/qt/src/mbgl/http_request.cpp b/platform/qt/src/mbgl/http_request.cpp index 0fb1993146a..973760a419a 100644 --- a/platform/qt/src/mbgl/http_request.cpp +++ b/platform/qt/src/mbgl/http_request.cpp @@ -49,6 +49,12 @@ QNetworkRequest HTTPRequest::networkRequest() const { req.setRawHeader("User-Agent", agent); #endif + if (m_resource.dataRange) { + std::string range = std::string("bytes=") + std::to_string(m_resource.dataRange->first) + std::string("-") + + std::to_string(m_resource.dataRange->second); + req.setRawHeader("Range", QByteArray(range.data(), static_cast(range.size()))); + } + if (m_resource.priorEtag) { const auto etag = m_resource.priorEtag; req.setRawHeader("If-None-Match", QByteArray(etag->data(), static_cast(etag->size()))); @@ -112,7 +118,8 @@ void HTTPRequest::handleNetworkReply(QNetworkReply* reply, const QByteArray& dat int responseCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); switch (responseCode) { - case 200: { + case 200: + case 206: { if (data.isEmpty()) { response.data = std::make_shared(); } else { diff --git a/platform/windows/windows.cmake b/platform/windows/windows.cmake index 8665a6f0be0..8e5a6493eb4 100644 --- a/platform/windows/windows.cmake +++ b/platform/windows/windows.cmake @@ -51,6 +51,7 @@ target_sources( ${PROJECT_SOURCE_DIR}/platform/default/src/mbgl/storage/offline_database.cpp ${PROJECT_SOURCE_DIR}/platform/default/src/mbgl/storage/offline_download.cpp ${PROJECT_SOURCE_DIR}/platform/default/src/mbgl/storage/online_file_source.cpp + ${PROJECT_SOURCE_DIR}/platform/default/src/mbgl/storage/$,pmtiles_file_source.cpp,pmtiles_file_source_stub.cpp> ${PROJECT_SOURCE_DIR}/platform/default/src/mbgl/storage/sqlite3.cpp ${PROJECT_SOURCE_DIR}/platform/default/src/mbgl/text/bidi.cpp ${PROJECT_SOURCE_DIR}/platform/default/src/mbgl/text/local_glyph_rasterizer.cpp diff --git a/src/mbgl/storage/pmtiles_file_source.hpp b/src/mbgl/storage/pmtiles_file_source.hpp new file mode 100644 index 00000000000..47d34139efe --- /dev/null +++ b/src/mbgl/storage/pmtiles_file_source.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include +#include +#include +#include + +namespace mbgl { +// File source for supporting .pmtiles maps +class PMTilesFileSource : public FileSource { +public: + PMTilesFileSource(const ResourceOptions& resourceOptions, const ClientOptions& clientOptions); + ~PMTilesFileSource() override; + + std::unique_ptr request(const Resource&, Callback) override; + bool canRequest(const Resource&) const override; + + void setResourceOptions(ResourceOptions) override; + ResourceOptions getResourceOptions() override; + + void setClientOptions(ClientOptions) override; + ClientOptions getClientOptions() override; + +private: + class Impl; + std::unique_ptr> thread; // impl +}; + +} // namespace mbgl diff --git a/src/mbgl/util/io.cpp b/src/mbgl/util/io.cpp index e66389e3319..4603c020935 100644 --- a/src/mbgl/util/io.cpp +++ b/src/mbgl/util/io.cpp @@ -46,14 +46,23 @@ std::string read_file(const std::string &filename) { } } -std::optional readFile(const std::string &filename) { +std::optional readFile(const std::string &filename, + const std::optional> &dataRange) { MLN_TRACE_FUNC(); std::ifstream file(filename, std::ios::binary); if (file.good()) { - std::stringstream data; - data << file.rdbuf(); - return data.str(); + if (dataRange) { + size_t size = static_cast(dataRange->second - dataRange->first + 1); + std::string data(size, '\0'); + file.seekg(static_cast(dataRange->first)); + file.read(&data[0], static_cast(size)); + return data; + } else { + std::stringstream data; + data << file.rdbuf(); + return data.str(); + } } return {}; } diff --git a/src/mbgl/util/io.hpp b/src/mbgl/util/io.hpp index 7e57b312798..bc08a7b925a 100644 --- a/src/mbgl/util/io.hpp +++ b/src/mbgl/util/io.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -15,7 +16,8 @@ struct IOException : std::runtime_error { void write_file(const std::string& filename, const std::string& data); std::string read_file(const std::string& filename); -std::optional readFile(const std::string& filename); +std::optional readFile(const std::string& filename, + const std::optional>& dataRange = std::nullopt); void deleteFile(const std::string& filename); void copyFile(const std::string& destination, const std::string& source); diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 816037eea13..2955aa3a51b 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -41,6 +41,7 @@ add_library( ${PROJECT_SOURCE_DIR}/test/storage/offline_database.test.cpp ${PROJECT_SOURCE_DIR}/test/storage/offline_download.test.cpp ${PROJECT_SOURCE_DIR}/test/storage/online_file_source.test.cpp + ${PROJECT_SOURCE_DIR}/test/storage/pmtiles_file_source.test.cpp ${PROJECT_SOURCE_DIR}/test/storage/resource.test.cpp ${PROJECT_SOURCE_DIR}/test/storage/sqlite.test.cpp ${PROJECT_SOURCE_DIR}/test/style/conversion/conversion_impl.test.cpp diff --git a/test/fixtures/storage/pmtiles/geography-class-png.pmtiles b/test/fixtures/storage/pmtiles/geography-class-png.pmtiles new file mode 100644 index 0000000000000000000000000000000000000000..aec0ca2a62904ae551f33af7e6b89e5437010189 GIT binary patch literal 88874 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�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�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 #include +#include +#include using namespace std::literals::string_literals; @@ -19,6 +21,7 @@ void runServer(std::unique_ptr& server) { using namespace httplib; server->Get("/test", [](const Request& req, Response& res) { + std::string content = "Hello World!"; if (req.has_param("modified")) { std::string str = util::rfc1123(util::parseTimestamp(std::stoi(req.get_param_value("modified")))); res.set_header("Last-Modified", str); @@ -33,7 +36,15 @@ void runServer(std::unique_ptr& server) { if (req.has_param("cachecontrol")) { res.set_header("Cache-Control", "max-age=" + req.get_param_value("cachecontrol")); } - res.set_content("Hello World!", "text/plain"); + if (req.has_param("range")) { + std::string str = req.get_param_value("range"); + str = str.substr(std::char_traits::length("bytes=")); + uint64_t start = std::strtoull(str.substr(0, str.find("-")).c_str(), nullptr, 10); + uint64_t end = std::strtoull(str.substr(str.find("-") + 1).c_str(), nullptr, 10); + content = content.substr(start, end - start + 1); + res.status = 206; + } + res.set_content(content, "text/plain"); }); server->Get("/stale", [](const Request&, Response&) { diff --git a/test/storage/http_file_source.test.cpp b/test/storage/http_file_source.test.cpp index 68c5e890bdb..e17a9489a31 100644 --- a/test/storage/http_file_source.test.cpp +++ b/test/storage/http_file_source.test.cpp @@ -37,6 +37,27 @@ TEST(HTTPFileSource, TEST_REQUIRES_SERVER(HTTP200)) { loop.run(); } +TEST(HTTPFileSource, TEST_REQUIRES_SERVER(HTTP206)) { + util::RunLoop loop; + HTTPFileSource fs(ResourceOptions::Default(), ClientOptions()); + + Resource resource(Resource::Unknown, "http://127.0.0.1:3000/test"); + resource.dataRange = std::make_pair(3, 8); + + auto req = fs.request(resource, [&](Response res) { + EXPECT_EQ(nullptr, res.error); + ASSERT_TRUE(res.data.get()); + EXPECT_EQ("lo Wor", *res.data); + EXPECT_FALSE(bool(res.expires)); + EXPECT_FALSE(res.mustRevalidate); + EXPECT_FALSE(bool(res.modified)); + EXPECT_FALSE(bool(res.etag)); + loop.stop(); + }); + + loop.run(); +} + TEST(HTTPFileSource, TEST_REQUIRES_SERVER(HTTP404)) { util::RunLoop loop; HTTPFileSource fs(ResourceOptions::Default(), ClientOptions()); diff --git a/test/storage/local_file_source.test.cpp b/test/storage/local_file_source.test.cpp index c845d5bf8be..e0e6c211b4b 100644 --- a/test/storage/local_file_source.test.cpp +++ b/test/storage/local_file_source.test.cpp @@ -5,6 +5,7 @@ #include #include +#include #include #if defined(WIN32) @@ -25,7 +26,7 @@ std::string toAbsoluteURL(const std::string& fileName) { #else char* cwd = getcwd(buff, PATH_MAX + 1); #endif - std::string url = {"file://" + std::string(cwd) + "/test/fixtures/storage/assets/" + fileName}; + std::string url = {mbgl::util::FILE_PROTOCOL + std::string(cwd) + "/test/fixtures/storage/assets/" + fileName}; assert(url.size() <= PATH_MAX); return url; } @@ -76,6 +77,25 @@ TEST(LocalFileSource, NonEmptyFile) { loop.run(); } +TEST(LocalFileSource, PartialFile) { + util::RunLoop loop; + + LocalFileSource fs(ResourceOptions::Default(), ClientOptions()); + + Resource resource(Resource::Unknown, toAbsoluteURL("nonempty")); + resource.dataRange = std::make_pair(4, 12); + + std::unique_ptr req = fs.request(resource, [&](Response res) { + req.reset(); + EXPECT_EQ(nullptr, res.error); + ASSERT_TRUE(res.data.get()); + EXPECT_EQ("ent is he", *res.data); + loop.stop(); + }); + + loop.run(); +} + TEST(LocalFileSource, NonExistentFile) { util::RunLoop loop; diff --git a/test/storage/pmtiles_file_source.test.cpp b/test/storage/pmtiles_file_source.test.cpp new file mode 100644 index 00000000000..abbe34fb0c2 --- /dev/null +++ b/test/storage/pmtiles_file_source.test.cpp @@ -0,0 +1,105 @@ +#include +#include +#include +#include +#include + +#include + +#include +#include + +namespace { + +std::string toAbsoluteURL(const std::string &fileName) { + auto path = std::filesystem::current_path() / "test/fixtures/storage/pmtiles" / fileName; + return std::string(mbgl::util::PMTILES_PROTOCOL) + std::string(mbgl::util::FILE_PROTOCOL) + path.string(); +} + +} // namespace + +using namespace mbgl; + +TEST(PMTilesFileSource, AcceptsURL) { + PMTilesFileSource pmtiles(ResourceOptions::Default(), ClientOptions()); + EXPECT_TRUE(pmtiles.canRequest(Resource::style("pmtiles:///test"))); + EXPECT_FALSE(pmtiles.canRequest(Resource::style("pmtile://test"))); + EXPECT_FALSE(pmtiles.canRequest(Resource::style("pmtiles:"))); + EXPECT_FALSE(pmtiles.canRequest(Resource::style(""))); +} + +// Nonexistent pmtiles file raises error +TEST(PMTilesFileSource, NonExistentFile) { + util::RunLoop loop; + + PMTilesFileSource pmtiles(ResourceOptions::Default(), ClientOptions()); + + std::unique_ptr req = pmtiles.request( + {Resource::Unknown, toAbsoluteURL("does_not_exist")}, [&](Response res) { + req.reset(); + ASSERT_NE(nullptr, res.error); + EXPECT_EQ(Response::Error::Reason::NotFound, res.error->reason); + EXPECT_NE((res.error->message).find("path not found"), std::string::npos); + ASSERT_FALSE(res.data.get()); + loop.stop(); + }); + + loop.run(); +} + +// Existing pmtiles file default request returns TileJSON +TEST(PMTilesFileSource, TileJSON) { + util::RunLoop loop; + + PMTilesFileSource pmtiles(ResourceOptions::Default(), ClientOptions()); + + std::unique_ptr req = pmtiles.request( + {Resource::Unknown, toAbsoluteURL("geography-class-png.pmtiles")}, [&](Response res) { + req.reset(); + EXPECT_EQ(nullptr, res.error); + ASSERT_TRUE(res.data.get()); + // basic test that TileJSON included a tile URL + EXPECT_NE((*res.data).find("geography-class-png.pmtiles"), std::string::npos); + loop.stop(); + }); + + loop.run(); +} + +// Existing tiles return tile data +TEST(PMTilesFileSource, Tile) { + util::RunLoop loop; + + PMTilesFileSource pmtiles(ResourceOptions::Default(), ClientOptions()); + + std::unique_ptr req = pmtiles.request( + Resource::tile(toAbsoluteURL("geography-class-png.pmtiles"), 1.0, 0, 0, 0, Tileset::Scheme::XYZ), + [&](Response res) { + req.reset(); + EXPECT_EQ(nullptr, res.error); + ASSERT_TRUE(res.data.get()); + ASSERT_EQ(res.noContent, false); + loop.stop(); + }); + + loop.run(); +} + +// Nonexistent tiles do not raise errors, they simply return no content +TEST(PMTilesFileSource, NonExistentTile) { + util::RunLoop loop; + + PMTilesFileSource pmtiles(ResourceOptions::Default(), ClientOptions()); + + std::unique_ptr req = pmtiles.request( + Resource::tile(toAbsoluteURL("geography-class-png.pmtiles"), 1.0, 0, 0, 4, Tileset::Scheme::XYZ), + [&](Response res) { + req.reset(); + EXPECT_EQ(nullptr, res.error); + ASSERT_FALSE(res.data.get()); + ASSERT_EQ(res.noContent, true); + loop.stop(); + }); + + loop.run(); +} diff --git a/test/storage/server.js b/test/storage/server.js index a7de0944d4a..04ccc430bfc 100755 --- a/test/storage/server.js +++ b/test/storage/server.js @@ -15,6 +15,7 @@ var app = express(); app.disable('etag'); app.get('/test', function (req, res) { + var content = 'Hello World!'; if (req.query.modified) { res.setHeader('Last-Modified', (new Date(req.query.modified * 1000)).toUTCString()); } @@ -27,7 +28,12 @@ app.get('/test', function (req, res) { if (req.query.cachecontrol) { res.setHeader('Cache-Control', req.query.cachecontrol); } - res.send('Hello World!'); + if (req.range()) { + const [ range ] = req.range(); + content = content.substring(range.start, range.end + 1); + res.status(206); + } + res.send(content); }); app.get('/stale/*', function() { diff --git a/vendor/BUILD.bazel b/vendor/BUILD.bazel index 66815d83b44..78fbf69c6c8 100644 --- a/vendor/BUILD.bazel +++ b/vendor/BUILD.bazel @@ -154,6 +154,15 @@ cc_library( visibility = ["//visibility:public"], ) +# vendor/pmtiles-files.json +cc_library( + name = "pmtiles", + hdrs = ["PMTiles/cpp/pmtiles.hpp"], + copts = CPP_FLAGS, + includes = ["PMTiles/cpp"], + visibility = ["//visibility:public"], +) + # vendor/polylabel-files.json cc_library( name = "polylabel", diff --git a/vendor/PMTiles b/vendor/PMTiles new file mode 160000 index 00000000000..60f3331ca9d --- /dev/null +++ b/vendor/PMTiles @@ -0,0 +1 @@ +Subproject commit 60f3331ca9d5cfa4fcdf1e076a3d3059aebb17bb diff --git a/vendor/pmtiles.cmake b/vendor/pmtiles.cmake new file mode 100644 index 00000000000..cb31addee70 --- /dev/null +++ b/vendor/pmtiles.cmake @@ -0,0 +1,21 @@ +if(TARGET mbgl-vendor-pmtiles) + return() +endif() + +add_library( + mbgl-vendor-pmtiles INTERFACE +) + +target_include_directories( + mbgl-vendor-pmtiles SYSTEM + INTERFACE ${CMAKE_CURRENT_LIST_DIR}/PMTiles/cpp +) + +set_target_properties( + mbgl-vendor-pmtiles + PROPERTIES + INTERFACE_MAPLIBRE_NAME "pmtiles" + INTERFACE_MAPLIBRE_URL "https://github.com/protomaps/PMTiles" + INTERFACE_MAPLIBRE_AUTHOR "Protomaps LLC" + INTERFACE_MAPLIBRE_LICENSE ${CMAKE_CURRENT_LIST_DIR}/PMTiles/LICENSE +)