Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions bazel/core.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
184 changes: 184 additions & 0 deletions design-proposals/2025-07-05-custom-vector-source.md
Original file line number Diff line number Diff line change
@@ -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<string>, │
│ │ 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<void(const CanonicalTileID&)>;

class CustomVectorSource final : public Source {
public:
struct Options {
TileFunction fetchTileFunction;
TileFunction cancelTileFunction;
Range<uint8_t> zoomRange = {0, 18};
};

CustomVectorSource(std::string id, const Options& options);

void setTileData(const CanonicalTileID&,
const std::shared_ptr<const std::string>& 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.
65 changes: 65 additions & 0 deletions include/mbgl/style/sources/custom_vector_source.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
#pragma once

#include <mbgl/style/source.hpp>
#include <mbgl/util/range.hpp>

#include <cstdint>
#include <functional>
#include <memory>
#include <string>

namespace mbgl {

class OverscaledTileID;
class CanonicalTileID;
template <class T>
class Actor;

namespace style {

using TileFunction = std::function<void(const CanonicalTileID&)>;

class CustomVectorTileLoader;

enum class TileDataFormat : uint8_t {
MVT = 0,
};

class CustomVectorSource final : public Source {
public:
struct Options {
TileFunction fetchTileFunction;
TileFunction cancelTileFunction;
Range<uint8_t> zoomRange = {0, 18};
};

CustomVectorSource(std::string id, const Options& options);
~CustomVectorSource() final;

void loadDescription(FileSource&) final;
void setTileData(const CanonicalTileID&,
const std::shared_ptr<const std::string>& 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<Source> makeWeakPtr() override { return weakFactory.makeWeakPtr(); }

protected:
Mutable<Source::Impl> createMutable() const noexcept final;

private:
std::unique_ptr<Actor<CustomVectorTileLoader>> loader;
mapbox::base::WeakPtrFactory<Source> weakFactory{this};
};

template <>
inline bool Source::is<CustomVectorSource>() const {
return getType() == SourceType::CustomMVTVector;
}

} // namespace style
} // namespace mbgl
3 changes: 2 additions & 1 deletion include/mbgl/style/types.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ enum class SourceType : uint8_t {
Video,
Annotations,
Image,
CustomVector
CustomVector,
CustomMVTVector
};

enum class VisibilityType : bool {
Expand Down
1 change: 1 addition & 0 deletions platform/android/MapLibreAndroid/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ dependencies {
implementation(libs.okhttp3)
implementation(libs.timber)
implementation(libs.interpolator)
implementation(libs.kotlinxCoroutinesCore)

testImplementation(libs.junit)
testImplementation(libs.mockito)
Expand Down
2 changes: 2 additions & 0 deletions platform/android/MapLibreAndroid/src/cpp/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading