diff --git a/CMakeLists.txt b/CMakeLists.txt index 913c1f7cb..a34536e75 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -378,6 +378,7 @@ list(APPEND INCLUDE_FILES ${PROJECT_SOURCE_DIR}/include/mbgl/style/rotation.hpp ${PROJECT_SOURCE_DIR}/include/mbgl/style/source.hpp ${PROJECT_SOURCE_DIR}/include/mbgl/style/sources/custom_geometry_source.hpp + ${PROJECT_SOURCE_DIR}/include/mbgl/style/sources/custom_vector_source.hpp ${PROJECT_SOURCE_DIR}/include/mbgl/style/sources/geojson_source.hpp ${PROJECT_SOURCE_DIR}/include/mbgl/style/sources/image_source.hpp ${PROJECT_SOURCE_DIR}/include/mbgl/style/sources/raster_dem_source.hpp @@ -628,6 +629,8 @@ list(APPEND SRC_FILES ${PROJECT_SOURCE_DIR}/src/mbgl/renderer/renderer_state.cpp ${PROJECT_SOURCE_DIR}/src/mbgl/renderer/sources/render_custom_geometry_source.cpp ${PROJECT_SOURCE_DIR}/src/mbgl/renderer/sources/render_custom_geometry_source.hpp + ${PROJECT_SOURCE_DIR}/src/mbgl/renderer/sources/render_custom_vector_source.cpp + ${PROJECT_SOURCE_DIR}/src/mbgl/renderer/sources/render_custom_vector_source.hpp ${PROJECT_SOURCE_DIR}/src/mbgl/renderer/sources/render_geojson_source.cpp ${PROJECT_SOURCE_DIR}/src/mbgl/renderer/sources/render_geojson_source.hpp ${PROJECT_SOURCE_DIR}/src/mbgl/renderer/sources/render_image_source.cpp @@ -699,6 +702,8 @@ list(APPEND SRC_FILES ${PROJECT_SOURCE_DIR}/src/mbgl/style/conversion/transition_options.cpp ${PROJECT_SOURCE_DIR}/src/mbgl/style/custom_tile_loader.cpp ${PROJECT_SOURCE_DIR}/src/mbgl/style/custom_tile_loader.hpp + ${PROJECT_SOURCE_DIR}/src/mbgl/style/custom_vector_tile_loader.cpp + ${PROJECT_SOURCE_DIR}/src/mbgl/style/custom_vector_tile_loader.hpp ${PROJECT_SOURCE_DIR}/src/mbgl/style/expression/assertion.cpp ${PROJECT_SOURCE_DIR}/src/mbgl/style/expression/at.cpp ${PROJECT_SOURCE_DIR}/src/mbgl/style/expression/boolean_operator.cpp @@ -815,6 +820,9 @@ list(APPEND SRC_FILES ${PROJECT_SOURCE_DIR}/src/mbgl/style/sources/custom_geometry_source.cpp ${PROJECT_SOURCE_DIR}/src/mbgl/style/sources/custom_geometry_source_impl.cpp ${PROJECT_SOURCE_DIR}/src/mbgl/style/sources/custom_geometry_source_impl.hpp + ${PROJECT_SOURCE_DIR}/src/mbgl/style/sources/custom_vector_source.cpp + ${PROJECT_SOURCE_DIR}/src/mbgl/style/sources/custom_vector_source_impl.cpp + ${PROJECT_SOURCE_DIR}/src/mbgl/style/sources/custom_vector_source_impl.hpp ${PROJECT_SOURCE_DIR}/src/mbgl/style/sources/geojson_source.cpp ${PROJECT_SOURCE_DIR}/src/mbgl/style/sources/geojson_source_impl.cpp ${PROJECT_SOURCE_DIR}/src/mbgl/style/sources/geojson_source_impl.hpp @@ -869,6 +877,8 @@ list(APPEND SRC_FILES ${PROJECT_SOURCE_DIR}/src/mbgl/text/tagged_string.hpp ${PROJECT_SOURCE_DIR}/src/mbgl/tile/custom_geometry_tile.cpp ${PROJECT_SOURCE_DIR}/src/mbgl/tile/custom_geometry_tile.hpp + ${PROJECT_SOURCE_DIR}/src/mbgl/tile/custom_vector_tile.cpp + ${PROJECT_SOURCE_DIR}/src/mbgl/tile/custom_vector_tile.hpp ${PROJECT_SOURCE_DIR}/src/mbgl/tile/geojson_tile.cpp ${PROJECT_SOURCE_DIR}/src/mbgl/tile/geojson_tile.hpp ${PROJECT_SOURCE_DIR}/src/mbgl/tile/geojson_tile_data.hpp diff --git a/bazel/core.bzl b/bazel/core.bzl index bbbc4b7a9..63acfe931 100644 --- a/bazel/core.bzl +++ b/bazel/core.bzl @@ -285,6 +285,8 @@ MLN_CORE_SOURCE = [ "src/mbgl/renderer/renderer_state.cpp", "src/mbgl/renderer/sources/render_custom_geometry_source.cpp", "src/mbgl/renderer/sources/render_custom_geometry_source.hpp", + "src/mbgl/renderer/sources/render_custom_vector_source.cpp", + "src/mbgl/renderer/sources/render_custom_vector_source.hpp", "src/mbgl/renderer/sources/render_geojson_source.cpp", "src/mbgl/renderer/sources/render_geojson_source.hpp", "src/mbgl/renderer/sources/render_image_source.cpp", @@ -357,6 +359,8 @@ MLN_CORE_SOURCE = [ "src/mbgl/style/conversion/sprite.cpp", "src/mbgl/style/custom_tile_loader.cpp", "src/mbgl/style/custom_tile_loader.hpp", + "src/mbgl/style/custom_vector_tile_loader.cpp", + "src/mbgl/style/custom_vector_tile_loader.hpp", "src/mbgl/style/expression/assertion.cpp", "src/mbgl/style/expression/at.cpp", "src/mbgl/style/expression/boolean_operator.cpp", @@ -448,6 +452,9 @@ MLN_CORE_SOURCE = [ "src/mbgl/style/sources/custom_geometry_source.cpp", "src/mbgl/style/sources/custom_geometry_source_impl.cpp", "src/mbgl/style/sources/custom_geometry_source_impl.hpp", + "src/mbgl/style/sources/custom_vector_source.cpp", + "src/mbgl/style/sources/custom_vector_source_impl.cpp", + "src/mbgl/style/sources/custom_vector_source_impl.hpp", "src/mbgl/style/sources/geojson_source.cpp", "src/mbgl/style/sources/geojson_source_impl.cpp", "src/mbgl/style/sources/geojson_source_impl.hpp", @@ -497,6 +504,8 @@ MLN_CORE_SOURCE = [ "src/mbgl/text/harfbuzz.hpp", "src/mbgl/tile/custom_geometry_tile.cpp", "src/mbgl/tile/custom_geometry_tile.hpp", + "src/mbgl/tile/custom_vector_tile.cpp", + "src/mbgl/tile/custom_vector_tile.hpp", "src/mbgl/tile/geojson_tile.cpp", "src/mbgl/tile/geojson_tile.hpp", "src/mbgl/tile/geojson_tile_data.hpp", @@ -780,6 +789,7 @@ MLN_CORE_HEADERS = [ "include/mbgl/style/rotation.hpp", "include/mbgl/style/source.hpp", "include/mbgl/style/sources/custom_geometry_source.hpp", + "include/mbgl/style/sources/custom_vector_source.hpp", "include/mbgl/style/sources/geojson_source.hpp", "include/mbgl/style/sources/image_source.hpp", "include/mbgl/style/sources/raster_dem_source.hpp", diff --git a/design-proposals/2025-07-05-custom-vector-source.md b/design-proposals/2025-07-05-custom-vector-source.md new file mode 100644 index 000000000..ce07269b9 --- /dev/null +++ b/design-proposals/2025-07-05-custom-vector-source.md @@ -0,0 +1,184 @@ +# Custom Vector Source + +## Motivation + +MapLibre Native currently supports two ways to provide vector tile data: + +1. **URL-based sources** (`vector` source type) — tiles are fetched according to the protocol of the URL (`http://`, + `file://`, etc.). +2. **CustomGeometrySource** — accepts GeoJSON features programmatically, but internally re-encodes them into vector + tiles, adding overhead and limiting control over the tile encoding. + +This proposal comes from the need for an efficient and kotlin native way to asynchronously connect a tile data source +similar to vector tile. +CustomGeometrySource was considered, but it has some obvious drawbacks: + +1. Tile requests are in the format of a bounding box — since it's not a tile X/Y/Z it is pretty unclear what logic it + conforms to. + 1. Is it just a tile X/Y/Z but converted to geo bounds? + 2. Can it ever request data in a bounding box that is bigger than just one X/Y/Z tile + 3. Additional boilerplate of mapping geo bounds into tile X/Y/Z +2. Internally CustomGeometrySource spawns a 4 thread threadpool and runs tasks one by one inside. It's a good old + technique + but Kotlin has things like coroutines which are a more ergonomic way to handle suspendable operations (like network + requests). +3. CustomGeometrySource operates over GeoJSON. So in case of a vector tiles like format it needs to be converted to + GeoJSON and then internally it will be cut down into vector tile format back. + +## Proposed Change + +Introduce a new source type, `CustomVectorSource`, that lets the application deliver raw binary tile data (starting with +MVT) directly to the rendering pipeline. The source participates in the standard tile loading lifecycle — the core +requests tiles by `CanonicalTileID`, and the application responds asynchronously with encoded data or an error. + +### Architecture + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Platform layer (Kotlin) │ +│ │ +│ CustomVectorTileProvider.fetchTile(z, x, y) → TileData │ +│ ↕ (JNI / platform bridge) │ +├─────────────────────────────────────────────────────────────┤ +│ Core C++ layer │ +│ │ +│ CustomVectorSource │ +│ ├── Options { fetchTileFunction, cancelTileFunction, │ +│ │ zoomRange } │ +│ ├── setTileData(CanonicalTileID, shared_ptr, │ +│ │ TileDataFormat) │ +│ ├── setTileError(CanonicalTileID, exception_ptr) │ +│ └── invalidateTile(CanonicalTileID) │ +│ │ +│ CustomVectorTileLoader (Actor on Scheduler::GetBackground) │ +│ └── Receives fetch/cancel requests from tiles │ +│ │ +│ CustomVectorTile (extends Tile) │ +│ └── Parses MVT data via VectorTileData, delegates to │ +│ standard GeometryTileWorker for symbol/line/fill │ +│ layout │ +│ │ +│ RenderCustomVectorSource (extends RenderTileSetSource) │ +│ └── Standard tile pyramid rendering │ +├─────────────────────────────────────────────────────────────┤ +│ Existing rendering pipeline (unchanged) │ +│ GeometryTileWorker → Buckets → Drawables → GPU │ +└─────────────────────────────────────────────────────────────┘ +``` + +### Key Design Decisions + +1. **Binary data, not GeoJSON.** For the Kotlin layer to send data to C++, it needs to be marshaled into some + data format that can be passed across languages. One could come up with a new binary format, implement an encoder in + Kotlin and a decoder in C++. But it turns out MVT is already such a format — just a binary blob that can be passed + simply as a sized buffer. Existing infrastructure of `MVT` tile data is just reused as is. + +2. **Actor-based concurrency.** `CustomVectorTileLoader` runs on a background thread via MapLibre's Actor system. The + platform callback (fetch/cancel) is invoked on this thread; the platform layer is responsible for dispatching to its + own preferred context (e.g., coroutines on Android). + +3. **Cooperative cancellation.** When a tile is no longer needed (e.g., user panned away), the core calls + `cancelTileFunction`. The platform layer can abort in-flight work. + +4. **Standard tile pipeline reuse.** Once binary data arrives, it goes through the same `VectorTileData` → + `GeometryTileWorker` path as HTTP-fetched vector tiles. No custom rendering code is needed. + +5. **Extensible format enum.** `TileDataFormat` is an enum starting with `MVT`. Future formats (e.g., a hypothetical + compact binary format) can be added without API changes. And obvious next candidate is `MLT` format support in + CustomVectorSource. + +### Threading Model + +``` +Main thread: Source::update() → tile needs data + ↓ +Background Actor: CustomVectorTileLoader::fetchTile() + ↓ +Platform bridge: fetchTileFunction callback → platform code + ↓ (async, any thread) + setTileData() → mailbox → CustomVectorTile::setData() + ↓ +GeometryTileWorker: parse MVT, create buckets (background pool) + ↓ +Main thread: Tile ready → render +``` + +## API Modifications + +### Core C++ (public header) + +```cpp +namespace mbgl::style { + +enum class TileDataFormat : uint8_t { MVT = 0 }; + +using TileFunction = std::function; + +class CustomVectorSource final : public Source { +public: + struct Options { + TileFunction fetchTileFunction; + TileFunction cancelTileFunction; + Range zoomRange = {0, 18}; + }; + + CustomVectorSource(std::string id, const Options& options); + + void setTileData(const CanonicalTileID&, + const std::shared_ptr& data, + TileDataFormat format = TileDataFormat::MVT); + void setTileError(const CanonicalTileID&, std::exception_ptr error); + void invalidateTile(const CanonicalTileID&); +}; + +} // namespace mbgl::style +``` + +### Android (Kotlin) + +```kotlin +interface CustomVectorTileProvider { + suspend fun fetchTile(z: Int, x: Int, y: Int): TileData +} + +sealed class TileData { + class Mvt(val data: ByteArray) : TileData() +} + +class CustomVectorSource( + id: String, + provider: CustomVectorTileProvider, + scope: CoroutineScope, + minZoom: Int = 0, + maxZoom: Int = 18 +) : Source() +``` + +The Android API uses Kotlin coroutines for async tile fetching. The `CoroutineScope` is caller-provided. Meaning +`CustomVectorSource` can run on a threadpool but not limited to a threadpool. `CoroutineScope` abstracts it away. + +### iOS / macOS + +Not included in this initial implementation. The core C++ API is platform-agnostic and can be wrapped in Swift/Obj-C +following the same pattern. + +## Migration Plan and Compatibility + +All changes are **additive**. No existing APIs are modified or removed. The new source type is opt-in — applications +that don't use it are unaffected. + +- A new `SourceType::CustomMVTVector` enum value is added to the core. +- Existing `CustomGeometrySource` remains unchanged and is not deprecated. + +## Rejected Alternatives + +### Extend CustomGeometrySource to accept binary data + +`CustomGeometrySource` is designed around GeoJSON features and internally manages geometry encoding. Bolting binary tile +support onto it would conflate two different data models and complicate its already non-trivial implementation. + +### Use a local file source with a custom URL scheme + +This approach (e.g., `mbtiles://` or `custom://`) requires the application to write tile data to disk or implement a +custom `FileSource`. It adds I/O overhead, doesn't support cooperative cancellation, and is harder to integrate with +structured concurrency frameworks like Kotlin coroutines. diff --git a/include/mbgl/style/sources/custom_vector_source.hpp b/include/mbgl/style/sources/custom_vector_source.hpp new file mode 100644 index 000000000..203a9c91c --- /dev/null +++ b/include/mbgl/style/sources/custom_vector_source.hpp @@ -0,0 +1,65 @@ +#pragma once + +#include +#include + +#include +#include +#include +#include + +namespace mbgl { + +class OverscaledTileID; +class CanonicalTileID; +template +class Actor; + +namespace style { + +using TileFunction = std::function; + +class CustomVectorTileLoader; + +enum class TileDataFormat : uint8_t { + MVT = 0, +}; + +class CustomVectorSource final : public Source { +public: + struct Options { + TileFunction fetchTileFunction; + TileFunction cancelTileFunction; + Range zoomRange = {0, 18}; + }; + + CustomVectorSource(std::string id, const Options& options); + ~CustomVectorSource() final; + + void loadDescription(FileSource&) final; + void setTileData(const CanonicalTileID&, + const std::shared_ptr& data, + TileDataFormat format = TileDataFormat::MVT); + void setTileError(const CanonicalTileID&, std::exception_ptr error); + void invalidateTile(const CanonicalTileID&); + + class Impl; + const Impl& impl() const; + bool supportsLayerType(const mbgl::style::LayerTypeInfo*) const override; + mapbox::base::WeakPtr makeWeakPtr() override { return weakFactory.makeWeakPtr(); } + +protected: + Mutable createMutable() const noexcept final; + +private: + std::unique_ptr> loader; + mapbox::base::WeakPtrFactory weakFactory{this}; +}; + +template <> +inline bool Source::is() const { + return getType() == SourceType::CustomMVTVector; +} + +} // namespace style +} // namespace mbgl diff --git a/include/mbgl/style/types.hpp b/include/mbgl/style/types.hpp index 3955514e9..f4b0e54c0 100644 --- a/include/mbgl/style/types.hpp +++ b/include/mbgl/style/types.hpp @@ -15,7 +15,8 @@ enum class SourceType : uint8_t { Video, Annotations, Image, - CustomVector + CustomVector, + CustomMVTVector }; enum class VisibilityType : bool { diff --git a/platform/android/MapLibreAndroid/build.gradle.kts b/platform/android/MapLibreAndroid/build.gradle.kts index 95621aa89..56708debf 100644 --- a/platform/android/MapLibreAndroid/build.gradle.kts +++ b/platform/android/MapLibreAndroid/build.gradle.kts @@ -22,6 +22,7 @@ dependencies { implementation(libs.okhttp3) implementation(libs.timber) implementation(libs.interpolator) + implementation(libs.kotlinxCoroutinesCore) testImplementation(libs.junit) testImplementation(libs.mockito) diff --git a/platform/android/MapLibreAndroid/src/cpp/CMakeLists.txt b/platform/android/MapLibreAndroid/src/cpp/CMakeLists.txt index f8c20465a..61a178fbc 100644 --- a/platform/android/MapLibreAndroid/src/cpp/CMakeLists.txt +++ b/platform/android/MapLibreAndroid/src/cpp/CMakeLists.txt @@ -177,6 +177,8 @@ add_library(maplibre SHARED ${PROJECT_SOURCE_DIR}/style/position.hpp ${PROJECT_SOURCE_DIR}/style/sources/custom_geometry_source.cpp ${PROJECT_SOURCE_DIR}/style/sources/custom_geometry_source.hpp + ${PROJECT_SOURCE_DIR}/style/sources/custom_vector_source.cpp + ${PROJECT_SOURCE_DIR}/style/sources/custom_vector_source.hpp ${PROJECT_SOURCE_DIR}/style/sources/geojson_source.cpp ${PROJECT_SOURCE_DIR}/style/sources/geojson_source.hpp ${PROJECT_SOURCE_DIR}/style/sources/image_source.cpp diff --git a/platform/android/MapLibreAndroid/src/cpp/style/sources/custom_vector_source.cpp b/platform/android/MapLibreAndroid/src/cpp/style/sources/custom_vector_source.cpp new file mode 100644 index 000000000..8e1278b94 --- /dev/null +++ b/platform/android/MapLibreAndroid/src/cpp/style/sources/custom_vector_source.cpp @@ -0,0 +1,137 @@ +#include "custom_vector_source.hpp" +#include "attach_env.hpp" + +#include + +#include + +namespace mbgl { +namespace android { + +CustomVectorSource::CustomVectorSource(jni::JNIEnv& env, + const jni::String& sourceId, + jni::jint minZoom, + jni::jint maxZoom) + : Source(env, + std::make_unique( + jni::Make(env, sourceId), + mbgl::style::CustomVectorSource::Options{ + std::bind(&CustomVectorSource::fetchTile, this, std::placeholders::_1), + std::bind(&CustomVectorSource::cancelTile, this, std::placeholders::_1), + {static_cast(minZoom), static_cast(maxZoom)}})) {} + +CustomVectorSource::~CustomVectorSource() { + onRemovedFromMap(); +} + +void CustomVectorSource::fetchTile(const mbgl::CanonicalTileID& tileID) { + android::UniqueEnv _env = android::AttachEnv(); + + static auto& javaClass = jni::Class::Singleton(*_env); + static auto fetchTile = javaClass.GetMethod(*_env, "fetchTile"); + + if (!javaPeer) { + return; + } + + auto peer = jni::Cast(*_env, javaClass, javaPeer); + peer.Call(*_env, fetchTile, (int)tileID.z, (int)tileID.x, (int)tileID.y); +} + +void CustomVectorSource::cancelTile(const mbgl::CanonicalTileID& tileID) { + android::UniqueEnv _env = android::AttachEnv(); + + static auto& javaClass = jni::Class::Singleton(*_env); + static auto cancelTile = javaClass.GetMethod(*_env, "cancelTile"); + + if (!javaPeer) { + return; + } + + auto peer = jni::Cast(*_env, javaClass, javaPeer); + peer.Call(*_env, cancelTile, (int)tileID.z, (int)tileID.x, (int)tileID.y); +} + +void CustomVectorSource::onAddedToMap() { + android::UniqueEnv _env = android::AttachEnv(); + + static auto& javaClass = jni::Class::Singleton(*_env); + static auto onAddedToMap = javaClass.GetMethod(*_env, "onAddedToMap"); + + assert(javaPeer); + + auto peer = jni::Cast(*_env, javaClass, javaPeer); + peer.Call(*_env, onAddedToMap); +} + +void CustomVectorSource::onRemovedFromMap() { + android::UniqueEnv _env = android::AttachEnv(); + + static auto& javaClass = jni::Class::Singleton(*_env); + static auto onRemovedFromMap = javaClass.GetMethod(*_env, "onRemovedFromMap"); + + if (javaPeer) { + auto peer = jni::Cast(*_env, javaClass, javaPeer); + peer.Call(*_env, onRemovedFromMap); + } +} + +void CustomVectorSource::setTileData( + jni::JNIEnv& env, jni::jint z, jni::jint x, jni::jint y, const jni::Array& jData, jni::jint format) { + std::shared_ptr data; + jsize length = jData.Length(env); + if (length > 0) { + data = std::make_shared(length, char()); + jni::GetArrayRegion(env, *jData, 0, length, reinterpret_cast(&(*data)[0])); + } + + source.as()->setTileData( + CanonicalTileID(z, x, y), data, static_cast(format)); +} + +void CustomVectorSource::setTileError( + jni::JNIEnv& env, jni::jint z, jni::jint x, jni::jint y, const jni::String& message) { + auto msg = jni::Make(env, message); + source.as()->setTileError(CanonicalTileID(z, x, y), + std::make_exception_ptr(std::runtime_error(msg))); +} + +void CustomVectorSource::invalidateTile(jni::JNIEnv&, jni::jint z, jni::jint x, jni::jint y) { + source.as()->invalidateTile(CanonicalTileID(z, x, y)); +} + +void CustomVectorSource::addToMap(JNIEnv& env, + const jni::Object& obj, + mbgl::Map& map, + AndroidRendererFrontend& frontend) { + Source::addToMap(env, obj, map, frontend); + onAddedToMap(); +} + +bool CustomVectorSource::removeFromMap(JNIEnv& env, const jni::Object& source, mbgl::Map& map) { + bool successfullyRemoved = Source::removeFromMap(env, source, map); + if (successfullyRemoved) { + onRemovedFromMap(); + } + return successfullyRemoved; +} + +void CustomVectorSource::registerNative(jni::JNIEnv& env) { + static auto& javaClass = jni::Class::Singleton(env); + +#define METHOD(MethodPtr, name) jni::MakeNativePeerMethod(name) + + jni::RegisterNativePeer( + env, + javaClass, + "nativePtr", + jni::MakePeer, + "initialize", + "finalize", + METHOD(&CustomVectorSource::setTileData, "nativeSetTileData"), + METHOD(&CustomVectorSource::setTileError, "nativeSetTileError"), + METHOD(&CustomVectorSource::invalidateTile, "nativeInvalidateTile")); +} + +} // namespace android +} // namespace mbgl diff --git a/platform/android/MapLibreAndroid/src/cpp/style/sources/custom_vector_source.hpp b/platform/android/MapLibreAndroid/src/cpp/style/sources/custom_vector_source.hpp new file mode 100644 index 000000000..3f911e687 --- /dev/null +++ b/platform/android/MapLibreAndroid/src/cpp/style/sources/custom_vector_source.hpp @@ -0,0 +1,37 @@ +#pragma once + +#include "source.hpp" +#include +#include +#include + +namespace mbgl { +namespace android { + +class CustomVectorSource : public Source { +public: + using SuperTag = Source; + static constexpr auto Name() { return "org/maplibre/android/style/sources/CustomVectorSource"; } + + static void registerNative(jni::JNIEnv&); + + CustomVectorSource(jni::JNIEnv&, const jni::String&, jni::jint minZoom, jni::jint maxZoom); + ~CustomVectorSource(); + + bool removeFromMap(JNIEnv&, const jni::Object&, mbgl::Map&) override; + void addToMap(JNIEnv&, const jni::Object&, mbgl::Map&, AndroidRendererFrontend&) override; + + void fetchTile(const mbgl::CanonicalTileID& tileID); + void cancelTile(const mbgl::CanonicalTileID& tileID); + void onAddedToMap(); + void onRemovedFromMap(); + +private: + void setTileData( + jni::JNIEnv&, jni::jint z, jni::jint x, jni::jint y, const jni::Array& data, jni::jint format); + void setTileError(jni::JNIEnv&, jni::jint z, jni::jint x, jni::jint y, const jni::String& message); + void invalidateTile(jni::JNIEnv&, jni::jint z, jni::jint x, jni::jint y); +}; + +} // namespace android +} // namespace mbgl diff --git a/platform/android/MapLibreAndroid/src/cpp/style/sources/source.cpp b/platform/android/MapLibreAndroid/src/cpp/style/sources/source.cpp index 9f02ad79d..e06ba80d1 100644 --- a/platform/android/MapLibreAndroid/src/cpp/style/sources/source.cpp +++ b/platform/android/MapLibreAndroid/src/cpp/style/sources/source.cpp @@ -17,6 +17,7 @@ #include // Core Sources +#include #include #include #include @@ -29,6 +30,7 @@ #include "unknown_source.hpp" #include "vector_source.hpp" #include "custom_geometry_source.hpp" +#include "custom_vector_source.hpp" #include "raster_dem_source.hpp" namespace mbgl { @@ -319,6 +321,7 @@ void Source::registerNative(jni::JNIEnv& env) { UnknownSource::registerNative(env); VectorSource::registerNative(env); CustomGeometrySource::registerNative(env); + CustomVectorSource::registerNative(env); RasterDEMSource::registerNative(env); } } // namespace android diff --git a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/style/sources/CustomVectorSource.kt b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/style/sources/CustomVectorSource.kt new file mode 100644 index 000000000..ffa7b2158 --- /dev/null +++ b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/style/sources/CustomVectorSource.kt @@ -0,0 +1,89 @@ +package org.maplibre.android.style.sources + +import androidx.annotation.Keep +import androidx.annotation.UiThread +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.launch +import java.util.concurrent.ConcurrentHashMap + +@UiThread +class CustomVectorSource( + id: String, + private val provider: CustomVectorTileProvider, + private val scope: CoroutineScope, + minZoom: Int = 0, + maxZoom: Int = 18 +) : Source() { + + private val activeJobs = ConcurrentHashMap() + + init { + initialize(id, minZoom, maxZoom) + } + + fun setTileData(z: Int, x: Int, y: Int, data: TileData) { + nativeSetTileData(z, x, y, data.bytes, data.formatId) + } + + fun invalidateTile(z: Int, x: Int, y: Int) { + nativeInvalidateTile(z, x, y) + } + + @Keep + private fun fetchTile(z: Int, x: Int, y: Int) { + val tileId = TileID(z, x, y) + activeJobs.remove(tileId)?.cancel() + + val job = scope.launch(start = CoroutineStart.LAZY) { + try { + val tileData = provider.fetchTile(z, x, y) + nativeSetTileData(z, x, y, tileData.bytes, tileData.formatId) + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + nativeSetTileError(z, x, y, e.message ?: "Tile fetch failed") + } finally { + val self = coroutineContext[Job]!! + activeJobs.remove(tileId, self) + } + } + activeJobs[tileId] = job + job.start() + } + + @Keep + private fun cancelTile(z: Int, x: Int, y: Int) { + activeJobs.remove(TileID(z, x, y))?.cancel() + } + + @Keep + private fun onAddedToMap() { /* no-op: scope is user-provided */ } + + @Keep + private fun onRemovedFromMap() { + activeJobs.values.forEach { it.cancel() } + activeJobs.clear() + } + + @Keep + private external fun initialize(sourceId: String, minZoom: Int, maxZoom: Int) + + @Keep + private external fun nativeSetTileData(z: Int, x: Int, y: Int, data: ByteArray, format: Int) + + @Keep + private external fun nativeSetTileError(z: Int, x: Int, y: Int, message: String) + + @Keep + private external fun nativeInvalidateTile(z: Int, x: Int, y: Int) + + @Keep + @Throws(Throwable::class) + protected external fun finalize() + + internal data class TileID(val z: Int, val x: Int, val y: Int) + +} diff --git a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/style/sources/CustomVectorTileProvider.kt b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/style/sources/CustomVectorTileProvider.kt new file mode 100644 index 000000000..74fe4a132 --- /dev/null +++ b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/style/sources/CustomVectorTileProvider.kt @@ -0,0 +1,5 @@ +package org.maplibre.android.style.sources + +interface CustomVectorTileProvider { + suspend fun fetchTile(z: Int, x: Int, y: Int): TileData +} diff --git a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/style/sources/TileData.kt b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/style/sources/TileData.kt new file mode 100644 index 000000000..1116fdffc --- /dev/null +++ b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/style/sources/TileData.kt @@ -0,0 +1,17 @@ +package org.maplibre.android.style.sources + +sealed class TileData { + class Mvt(val data: ByteArray) : TileData() + + internal val formatId: Int get() = when (this) { + is Mvt -> FORMAT_MVT + } + + internal val bytes: ByteArray get() = when (this) { + is Mvt -> data + } + + companion object { + internal const val FORMAT_MVT = 0 + } +} diff --git a/platform/android/MapLibreAndroidTestApp/src/main/AndroidManifest.xml b/platform/android/MapLibreAndroidTestApp/src/main/AndroidManifest.xml index 7cdedd553..5ec701d12 100644 --- a/platform/android/MapLibreAndroidTestApp/src/main/AndroidManifest.xml +++ b/platform/android/MapLibreAndroidTestApp/src/main/AndroidManifest.xml @@ -746,6 +746,18 @@ android:name="android.support.PARENT_ACTIVITY" android:value=".activity.FeatureOverviewActivity" /> + + + + (R.id.fabToggle).setOnClickListener { + toggleSource() + } + } + + override fun onMapReady(map: MapLibreMap) { + maplibreMap = map + map.setStyle(Style.Builder().fromUri(TestStyles.DEMOTILES)) + } + + private fun toggleSource() { + val map = maplibreMap ?: return + val style = map.style ?: return + + if (sourceActive) { + style.removeLayer(LAYER_ID) + style.removeSource(SOURCE_ID) + sourceActive = false + Toast.makeText(this, "CustomVectorSource removed", Toast.LENGTH_SHORT).show() + } else { + val source = CustomVectorSource(SOURCE_ID, DiagonalTileProvider(), tileScope, minZoom = 0, maxZoom = 14) + val layer = LineLayer(LAYER_ID, SOURCE_ID).apply { + setSourceLayer(SOURCE_LAYER_NAME) + setProperties( + PropertyFactory.lineColor(Color.RED), + PropertyFactory.lineWidth(2f), + PropertyFactory.lineOpacity(0.8f) + ) + } + style.addSource(source) + style.addLayer(layer) + sourceActive = true + Toast.makeText(this, "CustomVectorSource added", Toast.LENGTH_SHORT).show() + } + } + + /** + * A fake tile provider that generates MVT tiles containing diagonal lines. + * Simulates async work with a small delay. + */ + private class DiagonalTileProvider : CustomVectorTileProvider { + override suspend fun fetchTile(z: Int, x: Int, y: Int): TileData { + // Simulate network latency + delay(50) + val mvtBytes = buildDiagonalMvtTile(z, x, y) + return TileData.Mvt(mvtBytes) + } + } + + override fun onStart() { super.onStart(); mapView.onStart() } + override fun onResume() { super.onResume(); mapView.onResume() } + override fun onPause() { super.onPause(); mapView.onPause() } + override fun onStop() { super.onStop(); mapView.onStop() } + override fun onDestroy() { tileScope.cancel(); super.onDestroy(); mapView.onDestroy() } + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + mapView.onSaveInstanceState(outState) + } + + companion object { + const val SOURCE_ID = "custom-vector-source" + const val LAYER_ID = "custom-vector-layer" + const val SOURCE_LAYER_NAME = "diagonal" + private const val EXTENT = 4096 + + /** + * Builds a minimal MVT tile with two diagonal lines forming an X. + * MVT protobuf encoding done by hand (no protobuf library needed). + */ + private fun buildDiagonalMvtTile(z: Int, x: Int, y: Int): ByteArray { + // Geometry: two lines forming an X across the tile + // Line 1: (0,0) -> (4096,4096) + // Line 2: (4096,0) -> (0,4096) + val geom1 = encodeMvtGeometry( + listOf(Pair(0, 0), Pair(EXTENT, EXTENT)) + ) + val geom2 = encodeMvtGeometry( + listOf(Pair(EXTENT, 0), Pair(0, EXTENT)) + ) + + // Also add a border rectangle + val border = encodeMvtGeometry( + listOf(Pair(0, 0), Pair(EXTENT, 0), Pair(EXTENT, EXTENT), Pair(0, EXTENT), Pair(0, 0)) + ) + + val feature1 = encodeMvtFeature(id = 1, geometry = geom1, geomType = 2) + val feature2 = encodeMvtFeature(id = 2, geometry = geom2, geomType = 2) + val feature3 = encodeMvtFeature(id = 3, geometry = border, geomType = 2) + + val layer = encodeMvtLayer( + name = SOURCE_LAYER_NAME, + extent = EXTENT, + features = listOf(feature1, feature2, feature3) + ) + + // Tile: repeated Layer layers = 3; + val tile = ByteArrayOutputStream() + writeTag(tile, 3, 2) // field 3, wire type 2 (length-delimited) + writeBytes(tile, layer) + return tile.toByteArray() + } + + private fun encodeMvtGeometry(points: List>): ByteArray { + val commands = mutableListOf() + for (i in points.indices) { + if (i == 0) { + // MoveTo command: id=1, count=1 + commands.add(commandInteger(1, 1)) + commands.add(zigzag(points[0].first)) + commands.add(zigzag(points[0].second)) + } else { + if (i == 1) { + // LineTo command: id=2, count=remaining points + commands.add(commandInteger(2, points.size - 1)) + } + val dx = points[i].first - points[i - 1].first + val dy = points[i].second - points[i - 1].second + commands.add(zigzag(dx)) + commands.add(zigzag(dy)) + } + } + return commands.flatMap { encodeVarint(it) }.toByteArray() + } + + private fun encodeMvtFeature(id: Long, geometry: ByteArray, geomType: Int): ByteArray { + val out = ByteArrayOutputStream() + // optional uint64 id = 1 + writeTag(out, 1, 0) + writeVarint(out, id) + // optional GeomType type = 3 + writeTag(out, 3, 0) + writeVarint(out, geomType.toLong()) + // repeated uint32 geometry = 4 (packed) + writeTag(out, 4, 2) + writeBytes(out, geometry) + return out.toByteArray() + } + + private fun encodeMvtLayer(name: String, extent: Int, features: List): ByteArray { + val out = ByteArrayOutputStream() + // required uint32 version = 15 + writeTag(out, 15, 0) + writeVarint(out, 2) + // required string name = 1 + writeTag(out, 1, 2) + writeBytes(out, name.toByteArray(Charsets.UTF_8)) + // repeated Feature features = 2 + for (feature in features) { + writeTag(out, 2, 2) + writeBytes(out, feature) + } + // optional uint32 extent = 5 + writeTag(out, 5, 0) + writeVarint(out, extent.toLong()) + return out.toByteArray() + } + + private fun commandInteger(id: Int, count: Int): Int = (id and 0x7) or (count shl 3) + + private fun zigzag(value: Int): Int = (value shl 1) xor (value shr 31) + + private fun encodeVarint(value: Int): List { + val bytes = mutableListOf() + var v = value.toLong() and 0xFFFFFFFFL + while (v > 0x7F) { + bytes.add(((v and 0x7F) or 0x80).toByte()) + v = v ushr 7 + } + bytes.add((v and 0x7F).toByte()) + return bytes + } + + private fun writeTag(out: ByteArrayOutputStream, fieldNumber: Int, wireType: Int) { + writeVarint(out, ((fieldNumber shl 3) or wireType).toLong()) + } + + private fun writeVarint(out: ByteArrayOutputStream, value: Long) { + var v = value + while (v > 0x7F) { + out.write(((v and 0x7F) or 0x80).toInt()) + v = v ushr 7 + } + out.write((v and 0x7F).toInt()) + } + + private fun writeBytes(out: ByteArrayOutputStream, data: ByteArray) { + writeVarint(out, data.size.toLong()) + out.write(data) + } + + private fun List.toByteArray(): ByteArray = ByteArray(size) { this[it] } + } +} diff --git a/platform/android/MapLibreAndroidTestApp/src/main/res/layout/activity_custom_vector_source.xml b/platform/android/MapLibreAndroidTestApp/src/main/res/layout/activity_custom_vector_source.xml new file mode 100644 index 000000000..682d82618 --- /dev/null +++ b/platform/android/MapLibreAndroidTestApp/src/main/res/layout/activity_custom_vector_source.xml @@ -0,0 +1,25 @@ + + + + + + + + diff --git a/platform/android/MapLibreAndroidTestApp/src/main/res/values/descriptions.xml b/platform/android/MapLibreAndroidTestApp/src/main/res/values/descriptions.xml index 663c890ca..824665d66 100644 --- a/platform/android/MapLibreAndroidTestApp/src/main/res/values/descriptions.xml +++ b/platform/android/MapLibreAndroidTestApp/src/main/res/values/descriptions.xml @@ -83,6 +83,7 @@ Resize a map rendered on a SurfaceView Blend an overlay on a map Example Custom Geometry Source + CustomVectorSource with synthetic MVT tiles Load a map without providing a style URI or JSON Load MVT data Load PMTiles data diff --git a/platform/android/MapLibreAndroidTestApp/src/main/res/values/titles.xml b/platform/android/MapLibreAndroidTestApp/src/main/res/values/titles.xml index 30fbaf929..b0ca59f73 100644 --- a/platform/android/MapLibreAndroidTestApp/src/main/res/values/titles.xml +++ b/platform/android/MapLibreAndroidTestApp/src/main/res/values/titles.xml @@ -79,6 +79,7 @@ SurfaceView resize Map overlay Grid Source + Custom Vector Source No Style URI/JSON Vector Tile source PMTiles diff --git a/platform/default/src/mbgl/storage/offline_download.cpp b/platform/default/src/mbgl/storage/offline_download.cpp index 6e840715c..29df87d1f 100644 --- a/platform/default/src/mbgl/storage/offline_download.cpp +++ b/platform/default/src/mbgl/storage/offline_download.cpp @@ -219,6 +219,7 @@ OfflineRegionStatus OfflineDownload::getStatus() const { case SourceType::Video: case SourceType::Annotations: case SourceType::CustomVector: + case SourceType::CustomMVTVector: break; } } @@ -333,6 +334,7 @@ void OfflineDownload::activateDownload() { case SourceType::Video: case SourceType::Annotations: case SourceType::CustomVector: + case SourceType::CustomMVTVector: break; } } diff --git a/src/mbgl/renderer/render_source.cpp b/src/mbgl/renderer/render_source.cpp index 9f352d9ab..6f0872f82 100644 --- a/src/mbgl/renderer/render_source.cpp +++ b/src/mbgl/renderer/render_source.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -49,6 +50,9 @@ std::unique_ptr RenderSource::create(const Immutable case SourceType::CustomVector: return std::make_unique(staticImmutableCast(impl), threadPool); + case SourceType::CustomMVTVector: + return std::make_unique(staticImmutableCast(impl), + threadPool); } // Not reachable, but placate GCC. diff --git a/src/mbgl/renderer/sources/render_custom_vector_source.cpp b/src/mbgl/renderer/sources/render_custom_vector_source.cpp new file mode 100644 index 000000000..7b88a8705 --- /dev/null +++ b/src/mbgl/renderer/sources/render_custom_vector_source.cpp @@ -0,0 +1,57 @@ +#include +#include +#include +#include +#include + +namespace mbgl { + +using namespace style; + +RenderCustomVectorSource::RenderCustomVectorSource(Immutable impl_, + const TaggedScheduler& threadPool_) + : RenderTileSource(std::move(impl_), threadPool_) { + tilePyramid.setObserver(this); +} + +const style::CustomVectorSource::Impl& RenderCustomVectorSource::impl() const { + return static_cast(*baseImpl); +} + +void RenderCustomVectorSource::update(Immutable baseImpl_, + const std::vector>& layers, + const bool needsRendering, + const bool needsRelayout, + const TileParameters& parameters) { + if (baseImpl != baseImpl_) { + std::swap(baseImpl, baseImpl_); + + auto newImpl = staticImmutableCast(baseImpl); + auto currentImpl = staticImmutableCast(baseImpl_); + if (*newImpl != *currentImpl) { + tilePyramid.clearAll(); + } + } + + enabled = needsRendering; + + auto tileLoader = impl().getTileLoader(); + if (!tileLoader) { + return; + } + + tilePyramid.update(layers, + needsRendering, + needsRelayout, + parameters, + *baseImpl, + util::tileSize_I, + impl().getZoomRange(), + {}, + [&](const OverscaledTileID& tileID, TileObserver* observer_) { + return std::make_unique( + tileID, impl().id, parameters, *tileLoader, observer_); + }); +} + +} // namespace mbgl diff --git a/src/mbgl/renderer/sources/render_custom_vector_source.hpp b/src/mbgl/renderer/sources/render_custom_vector_source.hpp new file mode 100644 index 000000000..86bcfe026 --- /dev/null +++ b/src/mbgl/renderer/sources/render_custom_vector_source.hpp @@ -0,0 +1,22 @@ +#pragma once + +#include +#include + +namespace mbgl { + +class RenderCustomVectorSource final : public RenderTileSource { +public: + explicit RenderCustomVectorSource(Immutable, const TaggedScheduler&); + + void update(Immutable, + const std::vector>&, + bool needsRendering, + bool needsRelayout, + const TileParameters&) override; + +private: + const style::CustomVectorSource::Impl& impl() const; +}; + +} // namespace mbgl diff --git a/src/mbgl/style/custom_vector_tile_loader.cpp b/src/mbgl/style/custom_vector_tile_loader.cpp new file mode 100644 index 000000000..1c310ad05 --- /dev/null +++ b/src/mbgl/style/custom_vector_tile_loader.cpp @@ -0,0 +1,114 @@ +#include +#include + +namespace mbgl { +namespace style { + +CustomVectorTileLoader::CustomVectorTileLoader(const TileFunction& fetchTileFn, const TileFunction& cancelTileFn) + : fetchTileFunction(fetchTileFn), + cancelTileFunction(cancelTileFn) {} + +void CustomVectorTileLoader::fetchTile(const OverscaledTileID& tileID, const ActorRef& tileRef) { + std::scoped_lock guard(dataMutex); + auto cachedTileData = dataCache.find(tileID.canonical); + if (cachedTileData != dataCache.end()) { + tileRef.invoke(&CustomVectorTile::setTileData, cachedTileData->second.first, cachedTileData->second.second); + } + auto tileCallbacks = tileCallbackMap.find(tileID.canonical); + if (tileCallbacks == tileCallbackMap.end()) { + auto tuple = std::make_tuple(tileID.overscaledZ, tileID.wrap, tileRef); + tileCallbackMap.insert({tileID.canonical, std::vector(1, tuple)}); + } else { + for (auto& iter : tileCallbacks->second) { + if (std::get<0>(iter) == tileID.overscaledZ && std::get<1>(iter) == tileID.wrap) { + std::get<2>(iter) = tileRef; + return; + } + } + tileCallbacks->second.emplace_back(std::make_tuple(tileID.overscaledZ, tileID.wrap, tileRef)); + } + if (cachedTileData == dataCache.end()) { + invokeTileFetch(tileID.canonical); + } +} + +void CustomVectorTileLoader::cancelTile(const OverscaledTileID& tileID) { + std::scoped_lock guard(dataMutex); + if (tileCallbackMap.contains(tileID.canonical)) { + invokeTileCancel(tileID.canonical); + // Erase so a subsequent fetchTile for the same tile re-issues the fetch. + // Intentionally diverges from CustomTileLoader which omits this — that is a + // latent bug there that needs a separate cleanup. + tileCallbackMap.erase(tileID.canonical); + } +} + +void CustomVectorTileLoader::removeTile(const OverscaledTileID& tileID) { + std::scoped_lock guard(dataMutex); + auto tileCallbacks = tileCallbackMap.find(tileID.canonical); + if (tileCallbacks == tileCallbackMap.end()) return; + for (auto iter = tileCallbacks->second.begin(); iter != tileCallbacks->second.end(); iter++) { + if (std::get<0>(*iter) == tileID.overscaledZ && std::get<1>(*iter) == tileID.wrap) { + tileCallbacks->second.erase(iter); + invokeTileCancel(tileID.canonical); + break; + } + } + if (tileCallbacks->second.empty()) { + tileCallbackMap.erase(tileCallbacks); + dataCache.erase(tileID.canonical); + } +} + +void CustomVectorTileLoader::setTileData(const CanonicalTileID& tileID, + const std::shared_ptr& data, + TileDataFormat format) { + std::scoped_lock guard(dataMutex); + auto iter = tileCallbackMap.find(tileID); + if (iter == tileCallbackMap.end()) return; + for (auto& tuple : iter->second) { + auto actor = std::get<2>(tuple); + actor.invoke(&CustomVectorTile::setTileData, data, format); + } + dataCache[tileID] = std::make_pair(data, format); +} + +void CustomVectorTileLoader::setTileError(const CanonicalTileID& tileID, std::exception_ptr error) { + std::scoped_lock guard(dataMutex); + auto iter = tileCallbackMap.find(tileID); + if (iter == tileCallbackMap.end()) return; + for (auto& tuple : iter->second) { + auto actor = std::get<2>(tuple); + actor.invoke(&CustomVectorTile::setTileError, error); + } + tileCallbackMap.erase(iter); + dataCache.erase(tileID); +} + +void CustomVectorTileLoader::invalidateTile(const CanonicalTileID& tileID) { + std::scoped_lock guard(dataMutex); + auto tileCallbacks = tileCallbackMap.find(tileID); + if (tileCallbacks == tileCallbackMap.end()) return; + for (auto& iter : tileCallbacks->second) { + auto actor = std::get<2>(iter); + actor.invoke(&CustomVectorTile::invalidateTileData); + invokeTileCancel(tileID); + } + tileCallbackMap.erase(tileCallbacks); + dataCache.erase(tileID); +} + +void CustomVectorTileLoader::invokeTileFetch(const CanonicalTileID& tileID) { + if (fetchTileFunction != nullptr) { + fetchTileFunction(tileID); + } +} + +void CustomVectorTileLoader::invokeTileCancel(const CanonicalTileID& tileID) { + if (cancelTileFunction != nullptr) { + cancelTileFunction(tileID); + } +} + +} // namespace style +} // namespace mbgl diff --git a/src/mbgl/style/custom_vector_tile_loader.hpp b/src/mbgl/style/custom_vector_tile_loader.hpp new file mode 100644 index 000000000..8612a9de5 --- /dev/null +++ b/src/mbgl/style/custom_vector_tile_loader.hpp @@ -0,0 +1,52 @@ +#pragma once + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace mbgl { + +class CustomVectorTile; + +namespace style { + +class CustomVectorTileLoader { +public: + CustomVectorTileLoader(const CustomVectorTileLoader&) = delete; + CustomVectorTileLoader& operator=(const CustomVectorTileLoader&) = delete; + + using OverscaledIDFunctionTuple = std::tuple>; + + CustomVectorTileLoader(const TileFunction& fetchTileFn, const TileFunction& cancelTileFn); + + void fetchTile(const OverscaledTileID& tileID, const ActorRef& tileRef); + void cancelTile(const OverscaledTileID& tileID); + void removeTile(const OverscaledTileID& tileID); + + void setTileData(const CanonicalTileID& tileID, + const std::shared_ptr& data, + TileDataFormat format); + void setTileError(const CanonicalTileID& tileID, std::exception_ptr error); + void invalidateTile(const CanonicalTileID&); + +private: + void invokeTileFetch(const CanonicalTileID& tileID); + void invokeTileCancel(const CanonicalTileID& tileID); + + TileFunction fetchTileFunction; + TileFunction cancelTileFunction; + std::unordered_map> tileCallbackMap; + std::map, TileDataFormat>> dataCache; + std::mutex dataMutex; +}; + +} // namespace style +} // namespace mbgl diff --git a/src/mbgl/style/sources/custom_vector_source.cpp b/src/mbgl/style/sources/custom_vector_source.cpp new file mode 100644 index 000000000..901cd2312 --- /dev/null +++ b/src/mbgl/style/sources/custom_vector_source.cpp @@ -0,0 +1,56 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace mbgl { +namespace style { + +CustomVectorSource::CustomVectorSource(std::string id, const CustomVectorSource::Options& options) + : Source(makeMutable(std::move(id), options)), + loader(std::make_unique>( + Scheduler::GetBackground(), options.fetchTileFunction, options.cancelTileFunction)) {} + +CustomVectorSource::~CustomVectorSource() = default; + +const CustomVectorSource::Impl& CustomVectorSource::impl() const { + return static_cast(*baseImpl); +} + +void CustomVectorSource::loadDescription(FileSource&) { + baseImpl = makeMutable(impl(), loader->self()); + loaded = true; + observer->onSourceLoaded(*this); +} + +bool CustomVectorSource::supportsLayerType(const mbgl::style::LayerTypeInfo* info) const { + return mbgl::underlying_type(Tile::Kind::Geometry) == mbgl::underlying_type(info->tileKind); +} + +void CustomVectorSource::setTileData(const CanonicalTileID& tileID, + const std::shared_ptr& data, + TileDataFormat format) { + loader->self().invoke(&CustomVectorTileLoader::setTileData, tileID, data, format); +} + +void CustomVectorSource::setTileError(const CanonicalTileID& tileID, std::exception_ptr error) { + loader->self().invoke(&CustomVectorTileLoader::setTileError, tileID, error); +} + +void CustomVectorSource::invalidateTile(const CanonicalTileID& tileID) { + loader->self().invoke(&CustomVectorTileLoader::invalidateTile, tileID); +} + +Mutable CustomVectorSource::createMutable() const noexcept { + return staticMutableCast(makeMutable(impl())); +} + +} // namespace style +} // namespace mbgl diff --git a/src/mbgl/style/sources/custom_vector_source_impl.cpp b/src/mbgl/style/sources/custom_vector_source_impl.cpp new file mode 100644 index 000000000..1d60766a4 --- /dev/null +++ b/src/mbgl/style/sources/custom_vector_source_impl.cpp @@ -0,0 +1,33 @@ +#include + +namespace mbgl { +namespace style { + +CustomVectorSource::Impl::Impl(std::string id_, const CustomVectorSource::Options& options) + : Source::Impl(SourceType::CustomMVTVector, std::move(id_)), + zoomRange(options.zoomRange), + loaderRef({}) {} + +CustomVectorSource::Impl::Impl(const Impl& impl, const ActorRef& loaderRef_) + : Source::Impl(impl), + zoomRange(impl.zoomRange), + loaderRef(loaderRef_) {} + +bool CustomVectorSource::Impl::operator!=(const Impl& other) const noexcept { + return zoomRange != other.zoomRange || bool(loaderRef) != bool(other.loaderRef); +} + +std::optional CustomVectorSource::Impl::getAttribution() const { + return {}; +} + +Range CustomVectorSource::Impl::getZoomRange() const { + return zoomRange; +} + +std::optional> CustomVectorSource::Impl::getTileLoader() const { + return loaderRef; +} + +} // namespace style +} // namespace mbgl diff --git a/src/mbgl/style/sources/custom_vector_source_impl.hpp b/src/mbgl/style/sources/custom_vector_source_impl.hpp new file mode 100644 index 000000000..36168918e --- /dev/null +++ b/src/mbgl/style/sources/custom_vector_source_impl.hpp @@ -0,0 +1,28 @@ +#pragma once + +#include +#include +#include +#include + +namespace mbgl { +namespace style { + +class CustomVectorSource::Impl : public Source::Impl { +public: + Impl(std::string id, const CustomVectorSource::Options& options); + Impl(const Impl&, const ActorRef&); + + std::optional getAttribution() const final; + + Range getZoomRange() const; + std::optional> getTileLoader() const; + bool operator!=(const Impl&) const noexcept; + +private: + Range zoomRange; + std::optional> loaderRef; +}; + +} // namespace style +} // namespace mbgl diff --git a/src/mbgl/tile/custom_vector_tile.cpp b/src/mbgl/tile/custom_vector_tile.cpp new file mode 100644 index 000000000..4b4282589 --- /dev/null +++ b/src/mbgl/tile/custom_vector_tile.cpp @@ -0,0 +1,63 @@ +#include +#include +#include +#include +#include +#include + +#include + +namespace mbgl { + +CustomVectorTile::CustomVectorTile(const OverscaledTileID& overscaledTileID, + std::string sourceID_, + const TileParameters& parameters, + ActorRef loader_, + TileObserver* observer_) + : GeometryTile(overscaledTileID, std::move(sourceID_), parameters, observer_), + necessity(TileNecessity::Optional), + loader(std::move(loader_)), + mailbox(std::make_shared(*Scheduler::GetCurrent())), + actorRef(*this, mailbox) {} + +CustomVectorTile::~CustomVectorTile() { + loader.invoke(&style::CustomVectorTileLoader::removeTile, id); +} + +void CustomVectorTile::setTileData(const std::shared_ptr& data, style::TileDataFormat format) { + if (obsolete) return; + + switch (format) { + case style::TileDataFormat::MVT: + GeometryTile::setData(data ? std::make_unique(data) : nullptr); + break; + default: + assert(false && "Unhandled TileDataFormat"); + GeometryTile::setData(nullptr); + break; + } +} + +void CustomVectorTile::setTileError(std::exception_ptr error) { + if (obsolete) return; + GeometryTile::setError(std::move(error)); +} + +void CustomVectorTile::invalidateTileData() { + stale = true; + observer->onTileChanged(*this); +} + +void CustomVectorTile::setNecessity(TileNecessity newNecessity) { + if (newNecessity != necessity || stale) { + necessity = newNecessity; + if (necessity == TileNecessity::Required) { + loader.invoke(&style::CustomVectorTileLoader::fetchTile, id, actorRef); + stale = false; + } else if (!isRenderable()) { + loader.invoke(&style::CustomVectorTileLoader::cancelTile, id); + } + } +} + +} // namespace mbgl diff --git a/src/mbgl/tile/custom_vector_tile.hpp b/src/mbgl/tile/custom_vector_tile.hpp new file mode 100644 index 000000000..e73d07efd --- /dev/null +++ b/src/mbgl/tile/custom_vector_tile.hpp @@ -0,0 +1,37 @@ +#pragma once + +#include +#include +#include + +namespace mbgl { + +class TileParameters; + +namespace style { +class CustomVectorTileLoader; +} // namespace style + +class CustomVectorTile : public GeometryTile { +public: + CustomVectorTile(const OverscaledTileID&, + std::string sourceID, + const TileParameters&, + ActorRef loader, + TileObserver* observer = nullptr); + ~CustomVectorTile() override; + + void setTileData(const std::shared_ptr& data, style::TileDataFormat format); + void setTileError(std::exception_ptr error); + void invalidateTileData(); + void setNecessity(TileNecessity) final; + +private: + bool stale = true; + TileNecessity necessity; + ActorRef loader; + std::shared_ptr mailbox; + ActorRef actorRef; +}; + +} // namespace mbgl