Skip to content

Events and logs

Events tell you what happens in a map as it runs, like a style finishing loading or a tile failing to load. This guide shows how to receive map events and the library’s logs.

Events come from the runtime, so one event handler, set with mln_runtime_set_event_handlerRuntimeHandle.setEventHandlerRuntimeHandle.SetEventHandlerAsyncRuntimeHandle.SetEventHandlermaplibre_native_ffi_runtime_set_event_handler_asyncRuntimeHandle.setEventHandlerRuntimeHandle.setEventHandlerRuntimeHandle.set_event_handlerRuntimeHandle::set_event_handlerRuntimeHandle.setEventHandlerRuntimeHandle.setEventHandlerRuntime.setEventHandler, receives the events of the runtime and all its maps. Set it before you create a map, so it receives the map’s first events.

Each event names its type and the map that sent it. The handler runs on a library thread, so hand longer work to another thread.

main.c
static void on_events(
void* user_data, const mln_event_batch_view* batch
) {
(void)user_data;
for (size_t i = 0; i < batch->event_count; i++) {
const mln_runtime_event* event = MLN_STRIDED_AT(
mln_runtime_event, batch->events, batch->event_size, i
);
if (event->source != map) continue;
switch (event->type) {
case MLN_RUNTIME_EVENT_MAP_STYLE_LOADED:
on_style_loaded();
break;
case MLN_RUNTIME_EVENT_MAP_LOADING_FAILED:
// The message is valid only during this callback.
on_loading_failed(event->message);
break;
default:
break;
}
}
}
mln_runtime_event_handler handler = {
.size = sizeof(mln_runtime_event_handler),
.callback = on_events,
};
mln_runtime_set_event_handler(runtime, &handler, NULL, NULL);
main.dart
// The handler runs in this isolate. The binding queues each call.
runtime.discarding.setEventHandler(
RuntimeEventHandler(
callback: (events) {
for (final event in events) {
if (event.source != map?.id) continue;
switch (event.type) {
case RuntimeEventType.mapStyleLoaded:
onStyleLoaded();
case RuntimeEventType.mapLoadingFailed:
onLoadingFailed(event.message);
}
}
},
),
);
Program.cs
// Runs on a library thread.
private static void OnEvents(RuntimeEvent[] events)
{
foreach (var e in events)
{
if (e.Source != map?.Id)
continue;
switch (e.Type)
{
case RuntimeEventType.MapStyleLoaded:
OnStyleLoaded();
break;
case RuntimeEventType.MapLoadingFailed:
OnLoadingFailed(e.Message);
break;
}
}
}
runtime.Discarding.SetEventHandler(
new RuntimeEventHandler(OnEvents)
);
main.go
// Runs on a library thread.
handleEvents := func(events []maplibre.RuntimeEvent) {
for _, event := range events {
if event.Source != m.ID() {
continue
}
switch event.Type {
case maplibre.RuntimeEventTypeMapStyleLoaded:
onStyleLoaded()
case maplibre.RuntimeEventTypeMapLoadingFailed:
onLoadingFailed(event.Message)
}
}
}
err := runtime.Discarding().SetEventHandler(
maplibre.RuntimeEventHandler{Callback: handleEvents},
)
if err != nil {
return err
}
main.vala
var handler = new RuntimeEventHandler ();
// The handler runs on the main context of the thread that
// set it.
handler.set_callback ((events) => {
foreach (var event in events) {
if (map == null || !map.is_event_source (event)) {
continue;
}
switch (event.get_event_type ()) {
case RuntimeEventType.MAP_STYLE_LOADED:
on_style_loaded ();
break;
case RuntimeEventType.MAP_LOADING_FAILED:
on_loading_failed (event.get_message ());
break;
default:
break;
}
}
});
runtime.set_event_handler_async.begin (handler);
Main.java
private void handleEvents(List<RuntimeEvent> events) {
for (RuntimeEvent event : events) {
if (map == null || event.getSource() != map.getId()) continue;
switch (event.getType().getRawValue()) {
case RuntimeEventType.MAP_STYLE_LOADED_VALUE ->
onStyleLoaded();
case RuntimeEventType.MAP_LOADING_FAILED_VALUE ->
onLoadingFailed(event.getMessage());
default -> {}
}
}
}
// Runs on a library thread.
runtime
.getDiscarding()
.setEventHandler(
new RuntimeEventHandler(this::handleEvents));
Main.kt
private fun handleEvents(events: List<RuntimeEvent>) {
for (event in events) {
if (event.source != map?.id) continue
when (event.type) {
RuntimeEventType.MAP_STYLE_LOADED -> onStyleLoaded()
RuntimeEventType.MAP_LOADING_FAILED ->
onLoadingFailed(event.message)
}
}
}
// Runs on a library thread.
runtime.discarding.setEventHandler(
RuntimeEventHandler(::handleEvents),
)
main.py
def on_events(events: tuple[mln.RuntimeEvent, ...]) -> None:
for event in events:
if map_handle is None or event.source != map_handle.id:
continue
match event.type:
case mln.RuntimeEventType.MAP_STYLE_LOADED:
on_style_loaded()
case mln.RuntimeEventType.MAP_LOADING_FAILED:
on_loading_failed(event.message)
# The handler runs on the binding's dispatcher thread.
runtime.discarding.set_event_handler(
mln.RuntimeEventHandler(on_events)
)
main.rs
// The handler runs on a library thread.
// It sends the events to your thread.
let (sender, batches) = mpsc::channel();
runtime.discarding().set_event_handler(
RuntimeEventHandler::new(move |events| {
let _ = sender.send(events);
}),
)?;
// On your thread, such as on each turn of your event loop:
for event in batches.try_iter().flatten() {
if event.source != map.id() {
continue;
}
match event.r#type {
RuntimeEventType::MapStyleLoaded => on_style_loaded(),
RuntimeEventType::MapLoadingFailed => {
on_loading_failed(&event.message)
}
_ => {}
}
}
main.swift
@MainActor private func handleEvents(_ events: [RuntimeEvent]) {
for event in events where event.source == map?.id {
switch event.type {
case .mapStyleLoaded:
onStyleLoaded()
case .mapLoadingFailed:
onLoadingFailed(event.message)
default:
break
}
}
}
// The handler runs on a library thread.
// It hands each batch to the main actor.
try runtime.discarding
.setEventHandler(RuntimeEventHandler { events in
Task { @MainActor in handleEvents(events) }
})
main.ts
// The handler runs on this thread. The binding queues each call.
runtime.discarding.setEventHandler({
callback: (events) => {
for (const event of events) {
if (event.source !== map?.id) continue;
switch (event.type) {
case RuntimeEventType.mapStyleLoaded:
onStyleLoaded();
break;
case RuntimeEventType.mapLoadingFailed:
onLoadingFailed(event.message);
break;
}
}
},
});
main.zig
fn onEvents(
_: ?*anyopaque,
events: []const maplibre.RuntimeEvent,
) anyerror!void {
for (events) |event| {
if (event.source != map.id()) continue;
switch (event.type) {
.map_style_loaded => onStyleLoaded(),
// The message is valid only during this callback.
.map_loading_failed => onLoadingFailed(event.message),
else => {},
}
}
}
try runtime.discarding().setEventHandler(
.{ .callback = onEvents },
null,
);

By default, a map sends every event. To get only the events you act on, set mln_map_options.event_maskMapOptions.eventMaskMapOptions.EventMaskMapOptions.EventMaskMaplibreNativeFfiMapOptions:event-maskMapOptions.eventMaskMapOptions.eventMaskMapOptions.event_maskMapOptions::event_maskMapOptions.eventMaskMapOptions.eventMaskMapOptions.event_mask when you create the map, or change it later with mln_map_set_event_maskMapHandle.setEventMaskMapHandle.SetEventMaskAsyncMapHandle.SetEventMaskmaplibre_native_ffi_map_set_event_mask_asyncMapHandle.setEventMaskMapHandle.setEventMaskMapHandle.set_event_maskMapHandle::set_event_maskMapHandle.setEventMaskMapHandle.setEventMaskMap.setEventMask. Events cover style and resource loading, rendering, and camera changes, and mln_runtime_event_typeRuntimeEventTypeRuntimeEventTypeRuntimeEventTypeMaplibreNativeFfiRuntimeEventTypeRuntimeEventTypeRuntimeEventTypeRuntimeEventTypeRuntimeEventTypeRuntimeEventTypeRuntimeEventTypeRuntimeEventType lists them all.

main.c
mln_map_options map_options = {
.size = sizeof(mln_map_options),
.fields = MLN_MAP_OPTION_EVENT_MASK,
.initial_extent =
{.width = 512, .height = 512, .scale_factor = 1.0},
.event_mask = MLN_RUNTIME_EVENT_FLAG_MAP_STYLE_LOADED |
MLN_RUNTIME_EVENT_FLAG_MAP_LOADING_FAILED,
};
mln_runtime_create_map(runtime, &map_options, &map, NULL, NULL);
main.dart
map = runtime.createMap(
MapOptions(
initialExtent: const LogicalExtent(
width: 512,
height: 512,
scaleFactor: 1,
),
eventMask:
RuntimeEventFlag.mapStyleLoaded |
RuntimeEventFlag.mapLoadingFailed,
),
);
Program.cs
map = runtime.CreateMap(
new MapOptions
{
InitialExtent = new LogicalExtent(512, 512, 1),
EventMask =
RuntimeEventFlag.MapStyleLoaded
| RuntimeEventFlag.MapLoadingFailed,
}
);
main.go
m, created, err := runtime.CreateMap(maplibre.MapOptions{
InitialExtent: maplibre.LogicalExtent{
Width: 512, Height: 512, ScaleFactor: 1,
},
EventMask: new(maplibre.RuntimeEventFlagMapStyleLoaded |
maplibre.RuntimeEventFlagMapLoadingFailed),
})
if err != nil {
return err
}
main.vala
var options = new MapOptions () {
initial_extent = { width: 512, height: 512, scale_factor: 1.0 },
event_mask = RuntimeEventFlag.MAP_STYLE_LOADED
| RuntimeEventFlag.MAP_LOADING_FAILED,
};
map = runtime.create_map (options);
Main.java
MapHandle created =
runtime.createMap(
MapOptions.of(LogicalExtent.of(512, 512, 1.0))
.withEventMask(
RuntimeEventFlag.MAP_STYLE_LOADED.or(
RuntimeEventFlag.MAP_LOADING_FAILED)));
Main.kt
val created =
runtime.createMap(
MapOptions(
initialExtent =
LogicalExtent(
width = 512u,
height = 512u,
scaleFactor = 1.0,
),
eventMask =
RuntimeEventFlag.MAP_STYLE_LOADED or
RuntimeEventFlag.MAP_LOADING_FAILED,
),
)
main.py
map_handle = runtime.create_map(
mln.MapOptions(
initial_extent=mln.LogicalExtent(
width=512, height=512, scale_factor=1.0
),
event_mask=mln.RuntimeEventFlag.MAP_STYLE_LOADED
| mln.RuntimeEventFlag.MAP_LOADING_FAILED,
)
)
main.rs
let map_options = MapOptions {
event_mask: Some(
RuntimeEventFlag::MAP_STYLE_LOADED
| RuntimeEventFlag::MAP_LOADING_FAILED,
),
..MapOptions::new(LogicalExtent::new(512, 512, 1.0))
};
let (map, created) = runtime.create_map(&map_options)?;
main.swift
let created = try runtime.createMap(options: MapOptions(
initialExtent: LogicalExtent(
width: 512,
height: 512,
scaleFactor: 1
),
eventMask: [.mapStyleLoaded, .mapLoadingFailed]
))
main.ts
map = runtime.createMap({
initialExtent: { width: 512, height: 512, scaleFactor: 1 },
eventMask:
RuntimeEventFlag.mapStyleLoaded |
RuntimeEventFlag.mapLoadingFailed,
});
main.zig
var created = try runtime.createMap(.{
.initial_extent = .{
.width = 512,
.height = 512,
.scale_factor = 1.0,
},
.event_mask = .{
.map_style_loaded = true,
.map_loading_failed = true,
},
}, null);
defer created.ready.deinit();
map = created.map;

To send the library’s logs to your logger, set a log callback with mln_log_set_callbacksetLogCallbackMaplibreNative.SetLogCallbackSetLogCallbackmaplibre_native_ffi_set_log_callbacksetLogCallbacksetLogCallbackset_log_callbackset_log_callbacksetLogCallbacksetLogCallbacksetLogCallback. One callback receives the logs of every runtime in the process.

main.c
static void on_log(
void* user_data, mln_log_severity severity, mln_log_event event,
int64_t code, mln_buffer_view message
) {
(void)user_data;
(void)event;
(void)code;
// The message is valid only during this callback.
write_log(severity, message);
}
mln_log_handler log_handler = {
.size = sizeof(mln_log_handler),
.callback = on_log,
};
mln_log_set_callback(&log_handler, NULL);
main.dart
// The callback runs in this isolate. The binding queues each call.
setLogCallback(
LogHandler(
callback: (severity, _, _, message) {
writeLog(severity, message);
},
),
);
Program.cs
// Runs on a library thread, and can't call the library.
MaplibreNative.SetLogCallback(
new LogHandler
{
Callback = (severity, _, _, message) =>
WriteLog(severity, message),
}
);
main.go
err = maplibre.SetLogCallback(maplibre.LogHandler{
// Runs on a library thread, and can't call the library.
Callback: func(
severity maplibre.LogSeverity,
_ maplibre.LogEvent,
_ int64,
message string,
) {
writeLog(severity, message)
},
})
if err != nil {
return err
}
main.vala
var logs = new LogHandler ();
// The callback runs on the main context of the thread that
// set it.
logs.set_callback ((severity, event, code, message) => {
write_log (severity, message);
});
set_log_callback (logs);
Main.java
// Runs on a library thread, and can't call the library.
MaplibreNative.setLogCallback(
LogHandler.of(
(severity, event, code, message) ->
writeLog(severity, message)));
Main.kt
// Runs on a library thread, and can't call the library.
setLogCallback(
LogHandler { severity, _, _, message ->
writeLog(severity, message)
},
)
main.py
# The callback runs on the binding's dispatcher thread.
def on_log(
severity: mln.LogSeverity,
event: mln.LogEvent,
code: int,
message: str,
) -> None:
write_log(severity, message)
mln.set_log_callback(mln.LogHandler(on_log))
main.rs
// The callback runs on a library
// thread, and can't call the library.
set_log_callback(LogHandler::new(
|severity, _event, _code, message| {
write_log(severity, message);
},
))?;
main.swift
// Runs on a library thread, and can't call the library.
try setLogCallback(handler: LogHandler { severity, _, _, message in
writeLog(severity, message)
})
main.ts
// The callback runs on this thread. The binding queues each call.
setLogCallback({
callback: (severity, _event, _code, message) => {
writeLog(severity, message);
},
});
main.zig
fn onLog(
_: ?*anyopaque,
severity: maplibre.LogSeverity,
_: maplibre.LogEvent,
_: i64,
message: []const u8,
) anyerror!void {
// The message is valid only during this callback.
writeLog(severity, message);
}
try maplibre.setLogCallback(.{ .callback = onLog }, null);