Skip to content

Network

The library downloads styles, tiles, fonts, and icons from the URLs in the style. This guide shows how to change those requests, serve resources from your own code, and tell the library when the device is offline. Transforms and resource providers apply to every map on the runtime.

To add headers to requests, such as an Authorization header, set a header transform with mln_runtime_set_http_header_transformRuntimeHandle.setHttpHeaderTransformRuntimeHandle.SetHttpHeaderTransformAsyncRuntimeHandle.SetHTTPHeaderTransformmaplibre_native_ffi_runtime_set_http_header_transform_asyncRuntimeHandle.setHttpHeaderTransformRuntimeHandle.setHttpHeaderTransformRuntimeHandle.set_http_header_transformRuntimeHandle::set_http_header_transformRuntimeHandle.setHttpHeaderTransformRuntimeHandle.setHttpHeaderTransformRuntime.setHttpHeaderTransform.

For fixed values, give the transform rules. Each rule matches a URL pattern and lists its headers. Match only URLs on your own server, so your token doesn’t go to other servers in the style.

For values that change, such as a token that expires, give the transform a callback instead. It sets the headers of each request.

In a browser and on OpenHarmony, serve the requests with a resource provider to add headers instead.

main.c
const mln_http_header header = {
.name = mln_buffer_view_from_cstr("Authorization"),
.value = mln_buffer_view_from_cstr("Bearer my-token"),
};
const mln_http_header_rule rule = {
.size = sizeof(mln_http_header_rule),
.flags = MLN_URL_MATCH_GLOB,
.url =
mln_buffer_view_from_cstr("https://tiles.example.com/**"),
.headers = &header,
.header_count = 1,
};
const mln_http_header_transform transform = {
.size = sizeof(mln_http_header_transform),
.rules = &rule,
.rule_count = 1,
};
mln_runtime_set_http_header_transform(
runtime, &transform, NULL, NULL
);
main.dart
// This binding takes rules only, because
// it can't answer on a library thread.
runtime.discarding.setHttpHeaderTransform(
HttpHeaderTransform(
rules: [
HttpHeaderRule(
flags: UrlMatchFlag.glob,
url: 'https://tiles.example.com/**',
headers: [
HttpHeader(
name: 'Authorization',
value: 'Bearer my-token',
),
],
),
],
),
);
Program.cs
runtime.Discarding.SetHttpHeaderTransform(
new HttpHeaderTransform
{
Rules =
[
new HttpHeaderRule
{
Flags = UrlMatchFlag.Glob,
Url = "https://tiles.example.com/**",
Headers =
[
new HttpHeader(
"Authorization",
"Bearer my-token"
),
],
},
],
}
);
main.go
headers := maplibre.HTTPHeaderTransform{
Rules: []maplibre.HTTPHeaderRule{{
Flags: maplibre.URLMatchFlagGlob,
URL: "https://tiles.example.com/**",
Headers: []maplibre.HTTPHeader{{
Name: "Authorization",
Value: "Bearer my-token",
}},
}},
}
err := runtime.Discarding().SetHTTPHeaderTransform(headers)
if err != nil {
return err
}
main.vala
// This library takes rules only, because it can't answer
// on a library thread.
var header = new HttpHeader () {
name = "Authorization",
value = "Bearer my-token",
};
var header_rule = new HttpHeaderRule () {
flags = UrlMatchFlag.GLOB,
url = "https://tiles.example.com/**",
};
header_rule.set_headers ({ header });
var headers = new HttpHeaderTransform ();
headers.set_rules ({ header_rule });
runtime.set_http_header_transform_async.begin (headers);
Main.java
HttpHeaderRule authorization =
HttpHeaderRule.of()
.withFlags(UrlMatchFlag.GLOB)
.withUrl("https://tiles.example.com/**")
.withHeaders(
List.of(
HttpHeader.of("Authorization")
.withValue("Bearer my-token")));
MaplibreNative.setHttpHeaderTransform(
runtime.getDiscarding(),
HttpHeaderTransform.of().withRules(List.of(authorization)));
Main.kt
runtime.discarding.setHttpHeaderTransform(
HttpHeaderTransform(
rules =
listOf(
HttpHeaderRule(
flags = UrlMatchFlag.GLOB,
url = "https://tiles.example.com/**",
headers =
listOf(
HttpHeader(
name = "Authorization",
value = "Bearer my-token",
),
),
),
),
),
)
main.py
# This binding takes rules only, because
# it can't answer on a library thread.
runtime.discarding.set_http_header_transform(
mln.HttpHeaderTransform(
rules=[
mln.HttpHeaderRule(
flags=mln.UrlMatchFlag.GLOB,
url="https://tiles.example.com/**",
headers=[
mln.HttpHeader(
name="Authorization",
value="Bearer my-token",
)
],
)
]
)
)
main.rs
let rule = HttpHeaderRule {
flags: UrlMatchFlag::GLOB,
url: "https://tiles.example.com/**".to_owned(),
headers: vec![HttpHeader {
name: "Authorization".to_owned(),
value: "Bearer my-token".to_owned(),
}],
..Default::default()
};
runtime.discarding().set_http_header_transform(
HttpHeaderTransform {
rules: vec![rule],
..Default::default()
},
)?;
main.swift
try runtime.discarding
.setHttpHeaderTransform(HttpHeaderTransform(rules: [
HttpHeaderRule(
flags: .glob,
url: "https://tiles.example.com/**",
headers: [HttpHeader(
name: "Authorization",
value: "Bearer my-token"
)]
),
]))
main.ts
// This binding takes rules only, because
// it can't answer on a library thread.
runtime.discarding.setHttpHeaderTransform({
rules: [
{
flags: UrlMatchFlag.glob,
url: "https://tiles.example.com/**",
headers: [
{ name: "Authorization", value: "Bearer my-token" },
],
},
],
});
main.zig
try runtime.discarding().setHttpHeaderTransform(allocator, .{
.rules = &.{.{
.flags = .{ .glob = true },
.url = "https://tiles.example.com/**",
.headers = &.{.{
.name = "Authorization",
.value = "Bearer my-token",
}},
}},
}, null);

To change a request’s URL, such as to add an API key or to use another server, set a resource transform with mln_runtime_set_resource_transformRuntimeHandle.setResourceTransformRuntimeHandle.SetResourceTransformAsyncRuntimeHandle.SetResourceTransformmaplibre_native_ffi_runtime_set_resource_transform_asyncRuntimeHandle.setResourceTransformRuntimeHandle.setResourceTransformRuntimeHandle.set_resource_transformRuntimeHandle::set_resource_transformRuntimeHandle.setResourceTransformRuntimeHandle.setResourceTransformRuntime.setResourceTransform.

To add query parameters, such as an API key, give the transform rules. Each rule matches a URL pattern and lists the parameters to add. As with headers, match only URLs on your own server.

main.c
const mln_url_query_parameter key = {
.name = mln_buffer_view_from_cstr("key"),
.value = mln_buffer_view_from_cstr("my-api-key"),
};
const mln_resource_transform_rule rule = {
.size = sizeof(mln_resource_transform_rule),
.flags = MLN_URL_MATCH_GLOB,
.url =
mln_buffer_view_from_cstr("https://tiles.example.com/**"),
.parameters = &key,
.parameter_count = 1,
};
const mln_resource_transform transform = {
.size = sizeof(mln_resource_transform),
.rules = &rule,
.rule_count = 1,
};
mln_runtime_set_resource_transform(
runtime, &transform, NULL, NULL
);
main.dart
runtime.discarding.setResourceTransform(
ResourceTransform(
rules: [
ResourceTransformRule(
flags: UrlMatchFlag.glob,
url: 'https://tiles.example.com/**',
parameters: [
UrlQueryParameter(name: 'key', value: 'my-api-key'),
],
),
],
),
);
Program.cs
runtime.Discarding.SetResourceTransform(
new ResourceTransform
{
Rules =
[
new ResourceTransformRule
{
Flags = UrlMatchFlag.Glob,
Url = "https://tiles.example.com/**",
Parameters =
[
new UrlQueryParameter(
"key",
"my-api-key"
),
],
},
],
}
);
main.go
keys := maplibre.ResourceTransform{
Rules: []maplibre.ResourceTransformRule{{
Flags: maplibre.URLMatchFlagGlob,
URL: "https://tiles.example.com/**",
Parameters: []maplibre.URLQueryParameter{{
Name: "key",
Value: "my-api-key",
}},
}},
}
err = runtime.Discarding().SetResourceTransform(keys)
if err != nil {
return err
}
main.vala
var parameter = new UrlQueryParameter () {
name = "key",
value = "my-api-key",
};
var query_rule = new ResourceTransformRule () {
flags = UrlMatchFlag.GLOB,
url = "https://tiles.example.com/**",
};
query_rule.set_parameters ({ parameter });
var query = new ResourceTransform ();
query.set_rules ({ query_rule });
runtime.set_resource_transform_async.begin (query);
Main.java
ResourceTransformRule apiKey =
ResourceTransformRule.of()
.withFlags(UrlMatchFlag.GLOB)
.withUrl("https://tiles.example.com/**")
.withParameters(
List.of(
UrlQueryParameter.of("key")
.withValue("my-api-key")));
runtime
.getDiscarding()
.setResourceTransform(
ResourceTransform.of().withRules(List.of(apiKey)));
Main.kt
runtime.discarding.setResourceTransform(
ResourceTransform(
rules =
listOf(
ResourceTransformRule(
flags = UrlMatchFlag.GLOB,
url = "https://tiles.example.com/**",
parameters =
listOf(
UrlQueryParameter(
name = "key",
value = "my-api-key",
),
),
),
),
),
)
main.py
runtime.discarding.set_resource_transform(
mln.ResourceTransform(
rules=[
mln.ResourceTransformRule(
flags=mln.UrlMatchFlag.GLOB,
url="https://tiles.example.com/**",
parameters=[
mln.UrlQueryParameter(
name="key",
value="my-api-key",
)
],
)
]
)
)
main.rs
let rule = ResourceTransformRule {
flags: UrlMatchFlag::GLOB,
url: "https://tiles.example.com/**".to_owned(),
parameters: vec![UrlQueryParameter {
name: "key".to_owned(),
value: "my-api-key".to_owned(),
}],
..Default::default()
};
runtime.discarding().set_resource_transform(
ResourceTransform {
rules: vec![rule],
..Default::default()
},
)?;
main.swift
try runtime.discarding
.setResourceTransform(ResourceTransform(rules: [
ResourceTransformRule(
flags: .glob,
url: "https://tiles.example.com/**",
parameters: [UrlQueryParameter(
name: "key",
value: "my-api-key"
)]
),
]))
main.ts
runtime.discarding.setResourceTransform({
rules: [
{
flags: UrlMatchFlag.glob,
url: "https://tiles.example.com/**",
parameters: [{ name: "key", value: "my-api-key" }],
},
],
});
main.zig
try runtime.discarding().setResourceTransform(allocator, .{
.rules = &.{.{
.flags = .{ .glob = true },
.url = "https://tiles.example.com/**",
.parameters = &.{.{
.name = "key",
.value = "my-api-key",
}},
}},
}, null);

For any other change, give the transform a callback instead. It answers each request with its new URL through mln_resource_transform_request_completeResourceTransformRequestHandle.completeResourceTransformRequestHandle.CompleteResourceTransformRequestHandle.Completemaplibre_native_ffi_resource_transform_request_completeResourceTransformRequestHandle.completeResourceTransformRequestHandle.completeResourceTransformRequestHandle.completeResourceTransformRequestHandle::completeResourceTransformRequestHandle.completeResourceTransformRequestHandle.completeResourceTransformRequest.complete.

main.c
static void add_api_key(
void* user_data, mln_resource_kind kind, mln_buffer_view url,
mln_resource_transform_request request
) {
(void)user_data;
(void)kind;
char buffer[4096];
mln_buffer_view new_url = with_api_key(url, buffer, sizeof(buffer));
// The answer ends the request whatever it returns. A failed one
// fails it.
mln_resource_transform_request_complete(request, new_url, NULL);
}
const mln_resource_transform transform = {
.size = sizeof(mln_resource_transform),
.callback = add_api_key,
};
mln_runtime_set_resource_transform(
runtime, &transform, NULL, NULL
);
main.dart
runtime.discarding.setResourceTransform(
ResourceTransform(
callback: (_, url, request) {
request.complete(withApiKey(url));
},
),
);
Program.cs
// Runs on a library thread.
runtime.Discarding.SetResourceTransform(
new ResourceTransform
{
Callback = (_, url, request) =>
request.Complete(WithApiKey(url)),
}
);
main.go
rewrite := maplibre.ResourceTransform{
Callback: func(
_ maplibre.ResourceKind,
url string,
request *maplibre.ResourceTransformRequestHandle,
) {
// The answer ends the request whatever it returns. A failed
// one fails the request.
if err := request.Complete(withAPIKey(url)); err != nil {
log.Print(err)
}
},
}
err = runtime.Discarding().SetResourceTransform(rewrite)
if err != nil {
return err
}
main.vala
var rewrite = new ResourceTransform ();
rewrite.set_callback ((kind, url, request) => {
try {
request.complete (with_api_key (url));
} catch (GLib.Error error) {
warning ("%s", error.message);
}
});
runtime.set_resource_transform_async.begin (rewrite);
Main.java
runtime
.getDiscarding()
.setResourceTransform(
ResourceTransform.of()
.withCallback(
(kind, url, request) ->
request.complete(withApiKey(url))));
Main.kt
runtime.discarding.setResourceTransform(
ResourceTransform { _, url, request ->
request.complete(withApiKey(url))
},
)
main.py
def add_api_key(
kind: mln.ResourceKind,
url: str,
request: mln.ResourceTransformRequestHandle,
) -> None:
request.complete(with_api_key(url))
runtime.discarding.set_resource_transform(
mln.ResourceTransform(add_api_key)
)
main.rs
runtime.discarding().set_resource_transform(
ResourceTransform::new(|_kind, url, request| {
// Dropping the request without
// an answer fails it.
let _ = request.complete(&with_api_key(url));
}),
)?;
main.swift
let transform = ResourceTransform { _, url, request in
// The answer ends the request whatever it returns. A failed one
// fails
// the request.
try? request.complete(url: withApiKey(url))
}
try runtime.discarding.setResourceTransform(transform)
main.ts
runtime.discarding.setResourceTransform({
callback: (_kind, url, request) => {
request.complete(withApiKey(url));
},
});
main.zig
fn addApiKey(
_: ?*anyopaque,
_: maplibre.ResourceKind,
url: []const u8,
request: maplibre.ResourceTransformRequest,
) anyerror!void {
var owned = request;
var buffer: [4096]u8 = undefined;
// Returning an error releases the
// request, which fails it.
try owned.complete(try withApiKey(url, &buffer), null);
}
try runtime.discarding().setResourceTransform(
allocator,
.{ .callback = addApiKey },
null,
);

To stop a request instead, such as when the callback can’t get a credential, fail it with mln_resource_transform_request_failResourceTransformRequestHandle.failResourceTransformRequestHandle.FailResourceTransformRequestHandle.Failmaplibre_native_ffi_resource_transform_request_failResourceTransformRequestHandle.failResourceTransformRequestHandle.failResourceTransformRequestHandle.failResourceTransformRequestHandle::failResourceTransformRequestHandle.failResourceTransformRequestHandle.failResourceTransformRequest.fail. A request that the callback releases without an answer also fails. An answer ends the request, even when the answer fails, so don’t release the request after you answer it.

You might not need code for this. The library already reads file:// URLs, bundled files through asset:// URLs and the runtime’s asset path, and tile archives through pmtiles:// and mbtiles:// URLs, so you can point the style at those.

For anything else, set a resource provider with mln_runtime_set_resource_providerRuntimeHandle.setResourceProviderRuntimeHandle.SetResourceProviderAsyncRuntimeHandle.SetResourceProvidermaplibre_native_ffi_runtime_set_resource_provider_asyncRuntimeHandle.setResourceProviderRuntimeHandle.setResourceProviderRuntimeHandle.set_resource_providerRuntimeHandle::set_resource_providerRuntimeHandle.setResourceProviderRuntimeHandle.setResourceProviderRuntime.setResourceProvider. Give your URLs a custom scheme in the style, such as app://tiles/{z}/{x}/{y}.pbf, and give the provider a route that matches it.

Answer each request with its bytes through mln_resource_request_completeResourceRequestHandle.completeResourceRequestHandle.CompleteResourceRequestHandle.Completemaplibre_native_ffi_resource_request_completeResourceRequestHandle.completeResourceRequestHandle.completeResourceRequestHandle.completeResourceRequestHandle::completeResourceRequestHandle.completeResourceRequestHandle.completeResourceRequest.complete, or pass it through with mln_resource_request_pass_throughResourceRequestHandle.passThroughResourceRequestHandle.PassThroughResourceRequestHandle.PassThroughmaplibre_native_ffi_resource_request_pass_throughResourceRequestHandle.passThroughResourceRequestHandle.passThroughResourceRequestHandle.pass_throughResourceRequestHandle::pass_throughResourceRequestHandle.passThroughResourceRequestHandle.passThroughResourceRequest.passThrough to let the library download it.

main.c
static void serve_tile(
void* user_data, const mln_resource_request_info* request,
mln_resource_request handle
) {
(void)user_data;
mln_buffer_view tile = find_tile(request->requested_url);
mln_resource_response response = {
.size = sizeof(mln_resource_response)
};
if (tile.size > 0) {
response.status = MLN_RESOURCE_RESPONSE_STATUS_OK;
response.bytes = tile.data;
response.byte_count = tile.size;
} else {
response.status = MLN_RESOURCE_RESPONSE_STATUS_NO_CONTENT;
}
mln_resource_request_complete(handle, &response, NULL);
}
const mln_resource_route route = {
.size = sizeof(mln_resource_route),
.kinds = MLN_RESOURCE_KIND_FLAG_TILE,
.flags = MLN_URL_MATCH_GLOB,
.url = mln_buffer_view_from_cstr("app://tiles/**"),
};
const mln_resource_provider provider = {
.size = sizeof(mln_resource_provider),
.callback = serve_tile,
.routes = &route,
.route_count = 1,
};
mln_runtime_set_resource_provider(runtime, &provider, NULL, NULL);
main.dart
runtime.discarding.setResourceProvider(
ResourceProvider(
routes: [
ResourceRoute(
kinds: ResourceKindFlag.tile,
flags: UrlMatchFlag.glob,
url: 'app://tiles/**',
),
],
callback: (request, handle) {
final tile = findTile(request.requestedUrl);
handle.complete(
tile == null
? ResourceResponse(
status: ResourceResponseStatus.noContent,
)
: ResourceResponse(
status: ResourceResponseStatus.ok,
bytes: tile,
),
);
},
),
);
Program.cs
runtime.Discarding.SetResourceProvider(
new ResourceProvider
{
Routes =
[
new ResourceRoute
{
Kinds = ResourceKindFlag.Tile,
Flags = UrlMatchFlag.Glob,
Url = "app://tiles/**",
},
],
// Runs on a library thread.
Callback = (request, handle) =>
handle.Complete(
FindTile(request.RequestedUrl) is { } tile
? new ResourceResponse
{
Status = ResourceResponseStatus.Ok,
Bytes = tile,
}
: new ResourceResponse
{
Status =
ResourceResponseStatus.NoContent,
}
),
}
);
main.go
provider := maplibre.ResourceProvider{
Routes: []maplibre.ResourceRoute{{
Kinds: maplibre.ResourceKindFlagTile,
Flags: maplibre.URLMatchFlagGlob,
URL: "app://tiles/**",
}},
Callback: func(
request maplibre.ResourceRequestInfo,
handle *maplibre.ResourceRequestHandle,
) {
response := maplibre.ResourceResponse{
Status: maplibre.ResourceResponseStatusNoContent,
}
if tile := findTile(request.RequestedURL); tile != nil {
response = maplibre.ResourceResponse{
Status: maplibre.ResourceResponseStatusOk,
Bytes: tile,
}
}
if err := handle.Complete(response); err != nil {
log.Print(err)
}
},
}
err = runtime.Discarding().SetResourceProvider(provider)
if err != nil {
return err
}
main.vala
var route = new ResourceRoute () {
kinds = ResourceKindFlag.TILE,
flags = UrlMatchFlag.GLOB,
url = "app://tiles/**",
};
var provider = new ResourceProvider ();
provider.set_routes ({ route });
provider.set_callback ((request, handle) => {
var tile = find_tile (request.get_requested_url ());
var response = new ResourceResponse () {
status = ResourceResponseStatus.NO_CONTENT,
};
if (tile != null) {
response.status = ResourceResponseStatus.OK;
response.bytes = tile;
}
try {
handle.complete (response);
} catch (GLib.Error error) {
warning ("%s", error.message);
}
});
runtime.set_resource_provider_async.begin (provider);
Main.java
// Runs on a library thread.
private void serveTile(
ResourceRequestInfo request, ResourceRequestHandle handle) {
byte[] tile = findTile(request.getRequestedUrl());
if (tile == null) {
handle.complete(
ResourceResponse.of()
.withStatus(ResourceResponseStatus.NO_CONTENT));
} else {
handle.complete(
ResourceResponse.of()
.withStatus(ResourceResponseStatus.OK)
.withBytes(tile));
}
}
ResourceRoute tiles =
ResourceRoute.of()
.withKinds(ResourceKindFlag.TILE)
.withFlags(UrlMatchFlag.GLOB)
.withUrl("app://tiles/**");
runtime
.getDiscarding()
.setResourceProvider(
ResourceProvider.of(this::serveTile)
.withRoutes(List.of(tiles)));
Main.kt
runtime.discarding.setResourceProvider(
ResourceProvider(
routes =
listOf(
ResourceRoute(
kinds = ResourceKindFlag.TILE,
flags = UrlMatchFlag.GLOB,
url = "app://tiles/**",
),
),
) { request, handle ->
val tile = findTile(request.requestedUrl)
handle.complete(
if (tile == null) {
ResourceResponse(status = ResourceResponseStatus.NO_CONTENT)
} else {
ResourceResponse(
status = ResourceResponseStatus.OK,
bytes = tile,
)
},
)
},
)
main.py
def serve_tile(
request: mln.ResourceRequestInfo,
handle: mln.ResourceRequestHandle,
) -> None:
tile = find_tile(request.requested_url)
if tile is None:
handle.complete(
mln.ResourceResponse(
status=mln.ResourceResponseStatus.NO_CONTENT
)
)
else:
handle.complete(
mln.ResourceResponse(
status=mln.ResourceResponseStatus.OK, bytes=tile
)
)
runtime.discarding.set_resource_provider(
mln.ResourceProvider(
serve_tile,
routes=[
mln.ResourceRoute(
kinds=mln.ResourceKindFlag.TILE,
flags=mln.UrlMatchFlag.GLOB,
url="app://tiles/**",
)
],
)
)
main.rs
let provider = ResourceProvider {
routes: vec![ResourceRoute {
kinds: ResourceKindFlag::TILE,
flags: UrlMatchFlag::GLOB,
url: "app://tiles/**".to_owned(),
}],
..ResourceProvider::new(|request, handle| {
let tile = find_tile(&request.requested_url);
let response = match &tile {
Some(bytes) => ResourceResponse {
status: ResourceResponseStatus::Ok,
bytes,
..Default::default()
},
None => ResourceResponse {
status: ResourceResponseStatus::NoContent,
..Default::default()
},
};
// Dropping the request without an answer fails it.
let _ = handle.complete(&response);
})
};
runtime.discarding().set_resource_provider(provider)?;
main.swift
try runtime.discarding.setResourceProvider(ResourceProvider(
callback: { request, handle in
let response = if let tile = findTile(request.requestedUrl) {
ResourceResponse(status: .ok, bytes: tile)
} else {
ResourceResponse(status: .noContent)
}
try? handle.complete(response: response)
},
routes: [ResourceRoute(
kinds: .tile,
flags: .glob,
url: "app://tiles/**"
)]
))
main.ts
runtime.discarding.setResourceProvider({
routes: [
{
kinds: ResourceKindFlag.tile,
flags: UrlMatchFlag.glob,
url: "app://tiles/**",
},
],
callback: (request, handle) => {
const tile = findTile(request.requestedUrl);
handle.complete(
tile === undefined
? { status: ResourceResponseStatus.noContent }
: { status: ResourceResponseStatus.ok, bytes: tile },
);
},
});
main.zig
fn serveTile(
_: ?*anyopaque,
request: maplibre.ResourceRequestInfo,
handle: maplibre.ResourceRequest,
) anyerror!void {
var owned = handle;
if (findTile(request.requested_url)) |tile| {
try owned.complete(.{ .status = .ok, .bytes = tile }, null);
} else {
try owned.complete(.{ .status = .no_content }, null);
}
}
try runtime.discarding().setResourceProvider(allocator, .{
.callback = serveTile,
.routes = &.{.{
.kinds = .{ .tile = true },
.flags = .{ .glob = true },
.url = "app://tiles/**",
}},
}, null);

For tiles that don’t come from a URL, such as data your code computes, add a custom source. The map asks your code for each tile it needs, by zoom, x, and y. A source added with mln_map_add_custom_geometry_sourceMapHandle.addCustomGeometrySourceMapHandle.AddCustomGeometrySourceAsyncMapHandle.AddCustomGeometrySourcemaplibre_native_ffi_map_add_custom_geometry_source_asyncMapHandle.addCustomGeometrySourceMapHandle.addCustomGeometrySourceMapHandle.add_custom_geometry_sourceMapHandle::add_custom_geometry_sourceMapHandle.addCustomGeometrySourceMapHandle.addCustomGeometrySourceMap.addCustomGeometrySource takes GeoJSON for each tile. For encoded vector tiles, add a custom vector source with mln_map_add_custom_vector_sourceMapHandle.addCustomVectorSourceMapHandle.AddCustomVectorSourceAsyncMapHandle.AddCustomVectorSourcemaplibre_native_ffi_map_add_custom_vector_source_asyncMapHandle.addCustomVectorSourceMapHandle.addCustomVectorSourceMapHandle.add_custom_vector_sourceMapHandle::add_custom_vector_sourceMapHandle.addCustomVectorSourceMapHandle.addCustomVectorSourceMap.addCustomVectorSource instead.

Add the source after the map’s style loads, then add a layer that draws it, as in Style.

Each request names its map and source. Answer it with the tile’s data through mln_map_set_custom_geometry_source_tile_dataMapHandle.setCustomGeometrySourceTileDataMapHandle.SetCustomGeometrySourceTileDataAsyncMapHandle.SetCustomGeometrySourceTileDatamaplibre_native_ffi_map_set_custom_geometry_source_tile_data_asyncMapHandle.setCustomGeometrySourceTileDataMapHandle.setCustomGeometrySourceTileDataMapHandle.set_custom_geometry_source_tile_dataMapHandle::set_custom_geometry_source_tile_dataMapHandle.setCustomGeometrySourceTileDataMapHandle.setCustomGeometrySourceTileDataMap.setCustomGeometrySourceTileData. When your data for a tile changes, invalidate the tile with mln_map_invalidate_custom_geometry_source_tileMapHandle.invalidateCustomGeometrySourceTileMapHandle.InvalidateCustomGeometrySourceTileAsyncMapHandle.InvalidateCustomGeometrySourceTilemaplibre_native_ffi_map_invalidate_custom_geometry_source_tile_asyncMapHandle.invalidateCustomGeometrySourceTileMapHandle.invalidateCustomGeometrySourceTileMapHandle.invalidate_custom_geometry_source_tileMapHandle::invalidate_custom_geometry_source_tileMapHandle.invalidateCustomGeometrySourceTileMapHandle.invalidateCustomGeometrySourceTileMap.invalidateCustomGeometrySourceTile, and the map asks for it again.

main.c
static void fetch_tile(
void* user_data, mln_map map, mln_buffer_view source_id,
mln_canonical_tile_id tile
) {
(void)user_data;
mln_map_set_custom_geometry_source_tile_data(
map, source_id, tile,
mln_buffer_view_from_cstr(geojson_for_tile(tile)), NULL, NULL
);
}
const mln_custom_geometry_source_options options = {
.size = sizeof(mln_custom_geometry_source_options),
.fetch_tile = fetch_tile,
};
mln_map_add_custom_geometry_source(
map, mln_buffer_view_from_cstr("app-data"), &options, NULL, NULL
);
main.dart
map.discarding.addCustomGeometrySource(
'app-data',
CustomGeometrySourceOptions(
fetchTile: (sourceMap, sourceId, tile) {
sourceMap.discarding.setCustomGeometrySourceTileData(
sourceId,
tile,
geojsonForTile(tile),
);
},
),
);
Program.cs
map.Discarding.AddCustomGeometrySource(
"app-data",
new CustomGeometrySourceOptions
{
// Runs on a library thread.
FetchTile = (sourceMap, sourceId, tile) =>
sourceMap.Discarding.SetCustomGeometrySourceTileData(
sourceId,
tile,
GeoJsonForTile(tile)
),
}
);
main.go
source := maplibre.CustomGeometrySourceOptions{
FetchTile: func(
m *maplibre.MapHandle, sourceID string, tile maplibre.CanonicalTileID,
) {
err := m.Discarding().SetCustomGeometrySourceTileData(
sourceID, tile, geojsonForTile(tile),
)
if err != nil {
log.Print(err)
}
},
}
err = m.Discarding().AddCustomGeometrySource("app-data", source)
if err != nil {
return err
}
main.vala
var source = new CustomGeometrySourceOptions ();
source.set_fetch_tile ((source_map, source_id, tile) => {
source_map.set_custom_geometry_source_tile_data_async.begin (
source_id, tile, geojson_for_tile (tile)
);
});
map.add_custom_geometry_source_async.begin ("app-data", source);
Main.java
map.getDiscarding()
.addCustomGeometrySource(
"app-data",
CustomGeometrySourceOptions.of(
(sourceMap, sourceId, tile) ->
sourceMap
.getDiscarding()
.setCustomGeometrySourceTileData(
sourceId, tile, geojsonForTile(tile))));
Main.kt
map.discarding.addCustomGeometrySource(
"app-data",
CustomGeometrySourceOptions(
fetchTile = { sourceMap, sourceId, tile ->
sourceMap.discarding.setCustomGeometrySourceTileData(
sourceId,
tile,
geojsonForTile(tile),
)
},
),
)
main.py
def fetch_tile(
map_handle: mln.MapHandle,
source_id: str,
tile: mln.CanonicalTileId,
) -> None:
map_handle.discarding.set_custom_geometry_source_tile_data(
source_id, tile, geojson_for_tile(tile)
)
map_handle.discarding.add_custom_geometry_source(
"app-data",
mln.CustomGeometrySourceOptions(fetch_tile=fetch_tile),
)
main.rs
let options =
CustomGeometrySourceOptions::new(|map, source_id, tile| {
let data = geojson_for_tile(tile);
let _ = map
.discarding()
.set_custom_geometry_source_tile_data(
source_id, tile, &data,
);
});
map.discarding()
.add_custom_geometry_source("app-data", options)?;
main.swift
try map.discarding.addCustomGeometrySource(
"app-data",
options: CustomGeometrySourceOptions(
fetchTile: { sourceMap, sourceId, tile in
try? sourceMap.discarding.setCustomGeometrySourceTileData(
sourceId,
tileId: tile,
data: geojsonForTile(tile)
)
}
)
)
main.ts
map.discarding.addCustomGeometrySource("app-data", {
fetchTile: (map, sourceId, tile) => {
map.discarding.setCustomGeometrySourceTileData(
sourceId,
tile,
geojsonForTile(tile),
);
},
});
main.zig
fn fetchTile(
_: ?*anyopaque,
map: *const maplibre.Map,
source_id: []const u8,
tile: maplibre.CanonicalTileId,
) anyerror!void {
try map.discarding().setCustomGeometrySourceTileData(
source_id,
tile,
geojsonForTile(tile),
null,
);
}
try map.discarding().addCustomGeometrySource(
"app-data",
.{ .fetch_tile = fetchTile },
null,
);

When the device loses its connection, set the network status to offline with mln_network_set_statussetNetworkStatusMaplibreNative.SetNetworkStatusSetNetworkStatusmaplibre_native_ffi_set_network_statussetNetworkStatussetNetworkStatusset_network_statusset_network_statussetNetworkStatussetNetworkStatussetNetworkStatus. The library then stops starting network requests and loads what it can from its cache. Set it back to online when the connection returns. The status covers every runtime in the process.

main.c
mln_network_set_status(MLN_NETWORK_STATUS_OFFLINE, NULL);
// Later, when the connection returns:
mln_network_set_status(MLN_NETWORK_STATUS_ONLINE, NULL);
main.dart
setNetworkStatus(NetworkStatus.offline);
// Later, when the connection returns:
setNetworkStatus(NetworkStatus.online);
Program.cs
MaplibreNative.SetNetworkStatus(NetworkStatus.Offline);
// Later, when the connection returns:
MaplibreNative.SetNetworkStatus(NetworkStatus.Online);
main.go
err = maplibre.SetNetworkStatus(maplibre.NetworkStatusOffline)
if err != nil {
return err
}
// Later, when the connection returns:
err = maplibre.SetNetworkStatus(maplibre.NetworkStatusOnline)
if err != nil {
return err
}
main.vala
set_network_status (NetworkStatus.OFFLINE);
// Later, when the connection returns:
set_network_status (NetworkStatus.ONLINE);
Main.java
MaplibreNative.setNetworkStatus(NetworkStatus.OFFLINE);
// Later, when the connection returns:
MaplibreNative.setNetworkStatus(NetworkStatus.ONLINE);
Main.kt
setNetworkStatus(NetworkStatus.OFFLINE)
// Later, when the connection returns:
setNetworkStatus(NetworkStatus.ONLINE)
main.py
mln.set_network_status(mln.NetworkStatus.OFFLINE)
# Later, when the connection returns:
mln.set_network_status(mln.NetworkStatus.ONLINE)
main.rs
set_network_status(NetworkStatus::Offline)?;
// Later, when the connection returns:
set_network_status(NetworkStatus::Online)?;
main.swift
try setNetworkStatus(.offline)
// Later, when the connection returns:
try setNetworkStatus(.online)
main.ts
setNetworkStatus(NetworkStatus.offline);
// Later, when the connection returns:
setNetworkStatus(NetworkStatus.online);
main.zig
try maplibre.setNetworkStatus(.offline, null);
// Later, when the connection returns:
try maplibre.setNetworkStatus(.online, null);