Skip to content

Create a map

You primarily work with three stateful objects:

To avoid a leak, release each of these when you’re done with it.

This guide creates a runtime and a map, releases them, and covers how calls complete and fail. Choose a render target covers the target.

Create a runtime. Without mln_runtime_optionsRuntimeOptionsRuntimeOptionsRuntimeOptionsMaplibreNativeFfiRuntimeOptionsRuntimeOptionsRuntimeOptionsRuntimeOptionsRuntimeOptionsRuntimeOptionsRuntimeOptionsRuntimeOptions, it uses the defaults. Usually, one runtime is all you need for all your maps.

main.c
mln_runtime runtime = MLN_HANDLE_NULL;
mln_runtime_create(NULL, &runtime, NULL);
main.dart
final runtime = RuntimeHandle.create();
Program.cs
await using var runtime = RuntimeHandle.Create();
main.go
runtime, err := maplibre.NewRuntime(nil)
if err != nil {
return err
}
main.vala
var runtime = new Runtime.create (null);
Main.java
RuntimeHandle runtime = RuntimeHandle.create(null).join();
Main.kt
val runtime = RuntimeHandle.create().await()
main.py
runtime = mln.RuntimeHandle.create()
main.rs
let runtime = RuntimeHandle::create(None)?;
main.swift
let runtime = try RuntimeHandle.create()
main.ts
await using runtime = await RuntimeHandle.create();
main.zig
var runtime = try maplibre.Runtime.create(null, null);

Create a map on the runtime with mln_map_optionsMapOptionsMapOptionsMapOptionsMaplibreNativeFfiMapOptionsMapOptionsMapOptionsMapOptionsMapOptionsMapOptionsMapOptionsMapOptions, and give it a size in logical pixels, a scale factor, and a map mode. The scale factor and the mode can’t change later. For the mode, see Choose a render target.

Wait for the map to be ready before you attach a render target.

main.c
mln_map_options map_options = {
.size = sizeof(mln_map_options),
.initial_extent =
{.width = 800, .height = 600, .scale_factor = 2.0},
.map_mode = MLN_MAP_MODE_CONTINUOUS,
};
mln_map map = MLN_HANDLE_NULL;
mln_runtime_create_map(runtime, &map_options, &map, &done, NULL);
main.dart
final map = runtime.createMap(
const MapOptions(
initialExtent: LogicalExtent(
width: 800,
height: 600,
scaleFactor: 2,
),
mapMode: MapMode.continuous,
),
);
await map.ready;
Program.cs
await using var map = runtime.CreateMap(
new MapOptions
{
InitialExtent = new LogicalExtent(800, 600, 2),
MapMode = MapMode.Continuous,
}
);
await map.Ready;
main.go
m, created, err := runtime.CreateMap(maplibre.MapOptions{
InitialExtent: maplibre.LogicalExtent{
Width: 800, Height: 600, ScaleFactor: 2,
},
MapMode: maplibre.MapModeContinuous,
})
if err != nil {
return err
}
if _, err := created.Await(ctx); err != nil {
return err
}
main.vala
var options = new MapOptions () {
initial_extent = { width: 800, height: 600, scale_factor: 2.0 },
map_mode = MapMode.CONTINUOUS,
};
var map = runtime.create_map (options);
yield map.ready_async ();
Main.java
MapHandle map =
runtime.createMap(
MapOptions.of(LogicalExtent.of(800, 600, 2.0))
.withMapMode(MapMode.CONTINUOUS));
map.getReady().join();
Main.kt
val map =
runtime.createMap(
MapOptions(
initialExtent =
LogicalExtent(
width = 800u,
height = 600u,
scaleFactor = 2.0,
),
mapMode = MapMode.CONTINUOUS,
),
)
map.ready.await()
main.py
map_handle = runtime.create_map(
mln.MapOptions(
initial_extent=mln.LogicalExtent(
width=800, height=600, scale_factor=2.0
),
map_mode=mln.MapMode.CONTINUOUS,
)
)
map_handle.ready.result()
main.rs
let map_options = MapOptions {
map_mode: MapMode::Continuous,
..MapOptions::new(LogicalExtent::new(800, 600, 2.0))
};
let (map, created) = runtime.create_map(&map_options)?;
created.await?;
main.swift
let map = try runtime.createMap(options: MapOptions(
initialExtent: LogicalExtent(
width: 800,
height: 600,
scaleFactor: 2
),
mapMode: .continuous
))
try await map.ready
main.ts
await using map = runtime.createMap({
initialExtent: { width: 800, height: 600, scaleFactor: 2 },
mapMode: MapMode.continuous,
});
await map.ready;
main.zig
const map_options = maplibre.MapOptions{
.initial_extent = .{
.width = 800,
.height = 600,
.scale_factor = 2.0,
},
.map_mode = .continuous,
};
const created = try runtime.createMap(map_options, null);
var map = created.map;
try created.ready.await(null);

Set the map’s style by URL with mln_map_set_style_urlMapHandle.setStyleUrlMapHandle.SetStyleUrlAsyncMapHandle.SetStyleURLmaplibre_native_ffi_map_set_style_url_asyncMapHandle.setStyleUrlMapHandle.setStyleUrlMapHandle.set_style_urlMapHandle::set_style_urlMapHandle.setStyleUrlMapHandle.setStyleUrlMap.setStyleUrl or by JSON with mln_map_set_style_jsonMapHandle.setStyleJsonMapHandle.SetStyleJsonAsyncMapHandle.SetStyleJSONmaplibre_native_ffi_map_set_style_json_asyncMapHandle.setStyleJsonMapHandle.setStyleJsonMapHandle.set_style_jsonMapHandle::set_style_jsonMapHandle.setStyleJsonMapHandle.setStyleJsonMap.setStyleJson. To work with the map style in detail, see Style.

main.c
mln_map_set_style_url(
map,
mln_buffer_view_from_cstr(
"https://demotiles.maplibre.org/style.json"
),
NULL, NULL
);
main.dart
map.discarding.setStyleUrl(
'https://demotiles.maplibre.org/style.json',
);
Program.cs
map.Discarding.SetStyleUrl(
"https://demotiles.maplibre.org/style.json"
);
main.go
err = m.Discarding().SetStyleURL(
"https://demotiles.maplibre.org/style.json",
)
if err != nil {
return err
}
main.vala
map.set_style_url_async.begin (
"https://demotiles.maplibre.org/style.json"
);
Main.java
map.getDiscarding()
.setStyleUrl("https://demotiles.maplibre.org/style.json");
Main.kt
map.discarding.setStyleUrl(
"https://demotiles.maplibre.org/style.json",
)
main.py
map_handle.discarding.set_style_url(
"https://demotiles.maplibre.org/style.json"
)
main.rs
map.discarding()
.set_style_url("https://demotiles.maplibre.org/style.json")?;
main.swift
try map.discarding
.setStyleUrl("https://demotiles.maplibre.org/style.json")
main.ts
map.discarding.setStyleUrl(
"https://demotiles.maplibre.org/style.json",
);
main.zig
try map.discarding().setStyleUrl(
"https://demotiles.maplibre.org/style.json",
null,
);

Most work is coordinated by the map runtime on its own thread, so most calls are asynchronous. The library queues your work and then reports the result, either with a callback or with your language’s idiom for asynchronous operations.

Operations happen in the order you send them, so you generally only need to wait for the result you need, not every call on the way.

main.c
static void on_camera_moved(void*, const mln_completion_result*) {
// The camera has moved.
}
const mln_completion moved = {
.size = sizeof(mln_completion), .callback = on_camera_moved
};
mln_map_update_camera(map, &update, &moved, NULL);
// Pass no completion to ignore the result.
mln_map_update_camera(map, &update, NULL, NULL);
main.dart
await map.updateCamera(update);
// Use the discarding variant to ignore the result.
map.discarding.updateCamera(update);
Program.cs
await map.UpdateCameraAsync(update);
// Use the discarding variant to ignore the result.
map.Discarding.UpdateCamera(update);
main.go
moved, err := m.UpdateCamera(update)
if err != nil {
return err
}
if _, err := moved.Await(ctx); err != nil {
return err
}
// Use the discarding variant to ignore the result.
if err := m.Discarding().UpdateCamera(update); err != nil {
return err
}
main.vala
yield map.update_camera_async (update);
// Start it without a callback to ignore the result.
map.update_camera_async.begin (update);
Main.java
// join() blocks until the result arrives. To continue without
// blocking, chain a stage such as thenRun() instead.
map.updateCamera(update).join();
// Use the discarding variant to ignore the result.
map.getDiscarding().updateCamera(update);
Main.kt
map.updateCamera(update).await()
// Use the discarding variant to ignore the result.
map.discarding.updateCamera(update)
main.py
map_handle.update_camera(update).result()
# Use the discarding variant to ignore the result.
map_handle.discarding.update_camera(update)
main.rs
map.update_camera(&update)?.await?;
// Use the discarding variant to ignore the result.
map.discarding().update_camera(&update)?;
main.swift
try await map.updateCamera(update)
// Use the discarding variant to ignore the result.
try map.discarding.updateCamera(update)
main.ts
await map.updateCamera(update);
// Use the discarding variant to ignore the result.
map.discarding.updateCamera(update);
main.zig
_ = try (try map.updateCamera(update, null)).await(null);
// Use the discarding variant to ignore the result.
try map.discarding().updateCamera(update, null);

Some information is reported to your integration as events. See Events and logs to listen for events.

Some calls fail immediately, and others report a failure through their asynchronous result. See your language’s API reference for how each call can fail. Every failure carries a mln_statusStatusStatusStatusMaplibreNativeFfiErrorStatusStatusStatusStatusStatusStatusStatus and a message.

main.c
static void on_camera_result(
void*, const mln_completion_result* result
) {
if (result->status != MLN_STATUS_OK) {
report_failure(result->diagnostic);
}
}
const mln_completion result = {
.size = sizeof(mln_completion), .callback = on_camera_result
};
mln_diagnostic diagnostic = {.size = sizeof(mln_diagnostic)};
const mln_status status =
mln_map_update_camera(map, &update, &result, &diagnostic);
if (status != MLN_STATUS_OK) {
report_failure(mln_buffer_view_from_cstr(diagnostic.message));
}
main.dart
try {
await map.updateCamera(update);
} on MaplibreException catch (error) {
// Its message describes the failure.
reportFailure(error);
}
Program.cs
try
{
await map.UpdateCameraAsync(update);
}
catch (MaplibreException error)
{
// Its message describes the failure.
ReportFailure(error);
}
main.go
jumped, err := m.UpdateCamera(update)
if err != nil {
// The call failed right away.
reportFailure(err)
} else if _, err := jumped.Await(ctx); err != nil {
// The result reports a failure.
reportFailure(err)
}
main.vala
try {
yield map.update_camera_async (update);
} catch (GLib.Error error) {
// Its message describes the failure.
report_failure (error);
}
Main.java
try {
map.updateCamera(update).join();
} catch (MaplibreException error) {
// The call failed right away. Its message describes the
// failure.
reportFailure(error);
} catch (CompletionException error) {
// The result failed, with a MaplibreException as the cause.
reportFailure(error.getCause());
}
Main.kt
try {
map.updateCamera(update).await()
} catch (error: MaplibreException) {
// Its message describes the failure.
reportFailure(error)
}
main.py
try:
map_handle.update_camera(update).result()
except mln.MaplibreError as error:
# Its message describes the failure.
report_failure(error)
main.rs
match map.update_camera(&update) {
// The call failed right away.
Err(error) => report_failure(&error),
Ok(completion) => {
// The result reports a failure.
if let Err(error) = completion.await {
report_failure(&error);
}
}
}
main.swift
do {
try await map.updateCamera(update)
} catch {
// Its message describes the failure.
reportFailure(error)
}
main.ts
try {
await map.updateCamera(update);
} catch (error) {
// Its message describes the failure.
reportFailure(error);
}
main.zig
var diagnostic: maplibre.Diagnostic = .{};
const jumped = map.updateCamera(update, &diagnostic) catch |err| {
// The call failed right away.
reportFailure(diagnostic.message());
return err;
};
_ = jumped.await(&diagnostic) catch |err| {
// The result reports a failure.
reportFailure(diagnostic.message());
return err;
};

Release the render session, the map, and the runtime, in that order. Where your language has scoped cleanup, such as await using or with, leaving the scope cleans them up in that order.

main.c
mln_render_session_dispose(session, NULL);
mln_map_dispose(map, NULL);
mln_runtime_release(runtime, &done, NULL);
main.dart
session.dispose();
map.dispose();
await runtime.release();
Program.cs
await using var session = await AttachTarget(map);
// Leaving the scope releases the session, the map, and the runtime, in
// that order, and waits for each.
main.go
if err := session.Dispose(); err != nil {
return err
}
if err := m.Dispose(); err != nil {
return err
}
runtimeReleased, err := runtime.Release()
if err != nil {
return err
}
if _, err := runtimeReleased.Await(ctx); err != nil {
return err
}
main.vala
// Releasing the last reference disposes each handle without
// waiting for it to retire. The session goes first, since it
// keeps the map, and the map keeps the runtime.
session = null;
map = null;
runtime = null;
Main.java
try {
// Render frames and change the map.
} finally {
// Release each one, and wait for it, in this order.
session.release().join();
map.release().join();
runtime.release().join();
}
Main.kt
// Each block releases its handle when it ends, and waits for it.
runtime.useReleasing {
map.useReleasing {
session.useReleasing {
// Render frames and change the map.
}
}
}
main.py
# Leaving the block disposes the session, the map, and the runtime, in
# that order, without waiting for them to retire.
with runtime, map_handle, session:
pass # Render frames and change the map.
main.rs
drop(session);
drop(map);
runtime.release()?.await?;
main.swift
try session.dispose()
try map.dispose()
try await runtime.release()
main.ts
await using session = await attachTarget(map);
// Leaving the scope releases the session, the map, and the runtime, in
// that order, and waits for each.
main.zig
try session.dispose(null);
try map.dispose(null);
try (try runtime.release(null)).await(null);