1use std::fmt;
2use std::marker::PhantomData;
3use std::str;
4
5use maplibre_native_ffi_core as maplibre_core;
6use maplibre_native_ffi_sys as sys;
7
8use crate::{Error, Result};
9pub use maplibre_core::events::{
10 CameraTransitionFinishedEvent, OfflineOperationCompletedEvent, OfflineRegionResponseErrorEvent,
11 OfflineRegionStatus, OfflineRegionStatusEvent, OfflineRegionTileCountLimitEvent,
12 RenderFrameEvent, RenderMapEvent, RenderingStats, RuntimeEventPayload, TileActionEvent, TileId,
13 UnknownRuntimeEventPayload,
14};
15pub(crate) use maplibre_core::{OfflineRegionDownloadState, RuntimeEventType};
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
21pub struct MapId(u64);
22
23impl MapId {
24 pub(crate) const fn new(value: u64) -> Self {
25 Self(value)
26 }
27
28 pub const fn get(self) -> u64 {
30 self.0
31 }
32}
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
36#[non_exhaustive]
37pub enum RuntimeEventSource {
38 Runtime,
39 Map(MapId),
40 UnknownMap,
41 Unknown {
44 source_type: u32,
45 source: u64,
46 },
47}
48
49impl RuntimeEventSource {
50 pub(crate) fn from_raw(source_type: u32, source: u64) -> Self {
51 match source_type {
52 sys::MLN_RUNTIME_EVENT_SOURCE_RUNTIME => Self::Runtime,
53 sys::MLN_RUNTIME_EVENT_SOURCE_MAP if source != 0 => Self::Map(MapId::new(source)),
56 sys::MLN_RUNTIME_EVENT_SOURCE_MAP => Self::UnknownMap,
57 source_type => Self::Unknown {
58 source_type,
59 source,
60 },
61 }
62 }
63}
64
65#[derive(Debug, Clone, PartialEq)]
67#[non_exhaustive]
68pub struct RuntimeEvent {
69 pub event_type: RuntimeEventType,
70 pub source: RuntimeEventSource,
71 pub code: i32,
77 pub message: Option<String>,
78 pub payload: RuntimeEventPayload,
79}
80
81pub struct RuntimeEventBatch<'a> {
88 raw: sys::mln_runtime_event_batch,
89 _storage: PhantomData<&'a [u8]>,
90}
91
92impl<'a> RuntimeEventBatch<'a> {
93 pub(crate) unsafe fn new(raw: sys::mln_runtime_event_batch) -> Self {
100 Self {
101 raw,
102 _storage: PhantomData,
103 }
104 }
105
106 pub fn len(&self) -> usize {
108 self.raw.event_count
109 }
110
111 pub fn is_empty(&self) -> bool {
113 self.len() == 0
114 }
115
116 pub fn remaining(&self) -> usize {
119 self.raw.remaining_count
120 }
121
122 pub fn iter(&self) -> impl Iterator<Item = RuntimeEventRef<'_>> {
136 (0..self.len()).map(move |index| {
137 RuntimeEventRef {
140 view: unsafe { maplibre_core::events::event_view(&self.raw, index) },
141 }
142 })
143 }
144}
145
146impl fmt::Debug for RuntimeEventBatch<'_> {
147 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
148 f.debug_struct("RuntimeEventBatch")
149 .field("len", &self.len())
150 .field("remaining", &self.remaining())
151 .finish()
152 }
153}
154
155#[derive(Clone, Copy)]
157pub struct RuntimeEventRef<'a> {
158 view: maplibre_core::NativeEventView<'a>,
159}
160
161impl<'a> RuntimeEventRef<'a> {
162 pub fn event_type(&self) -> RuntimeEventType {
164 RuntimeEventType::from_raw(self.view.raw.type_)
165 }
166
167 pub fn source(&self) -> RuntimeEventSource {
169 RuntimeEventSource::from_raw(self.view.raw.source_type, self.view.raw.source)
170 }
171
172 pub fn code(&self) -> i32 {
175 self.view.raw.code
176 }
177
178 pub fn payload(&self) -> RuntimeEventPayload {
182 unsafe { maplibre_core::events::payload_from_view(&self.view) }
185 }
186
187 pub fn message(&self) -> Result<Option<&'a str>> {
190 if self.view.message.is_empty() {
191 return Ok(None);
192 }
193 str::from_utf8(self.view.message)
194 .map(Some)
195 .map_err(|error| {
196 Error::invalid_argument(format!(
197 "runtime event message was not valid UTF-8: {error}"
198 ))
199 })
200 }
201
202 pub fn message_bytes(&self) -> &'a [u8] {
205 self.view.message
206 }
207
208 pub fn to_owned(&self) -> Result<RuntimeEvent> {
210 let copied = unsafe { maplibre_core::events::copied_event_from_view(&self.view) }?;
213 Ok(RuntimeEvent {
214 event_type: copied.event_type,
215 source: RuntimeEventSource::from_raw(
216 copied.source.source_type,
217 copied.source.source_id,
218 ),
219 code: copied.code,
220 message: copied.message,
221 payload: copied.payload,
222 })
223 }
224}
225
226impl fmt::Debug for RuntimeEventRef<'_> {
227 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
228 f.debug_struct("RuntimeEventRef")
229 .field("event_type", &self.event_type())
230 .field("source", &self.source())
231 .field("code", &self.code())
232 .field("message_bytes", &self.message_bytes())
233 .finish()
234 }
235}
236
237#[cfg(test)]
240pub(crate) struct SynthesizedBatch {
241 records: Vec<u8>,
242 messages: Vec<u8>,
243 stride: usize,
244 count: usize,
245}
246
247#[cfg(test)]
248impl SynthesizedBatch {
249 pub(crate) fn new() -> Self {
250 Self {
251 records: Vec::new(),
252 messages: Vec::new(),
253 stride: std::mem::size_of::<sys::mln_runtime_event>(),
254 count: 0,
255 }
256 }
257
258 pub(crate) fn zeroed_event(event_type: u32) -> sys::mln_runtime_event {
261 let mut event = unsafe { std::mem::zeroed::<sys::mln_runtime_event>() };
263 event.type_ = event_type;
264 event
265 }
266
267 pub(crate) fn push(&mut self, mut event: sys::mln_runtime_event, message: &[u8]) {
268 event.message_offset = u32::try_from(self.messages.len()).unwrap();
269 event.message_size = u32::try_from(message.len()).unwrap();
270 self.messages.extend_from_slice(message);
271 self.messages.push(0);
272
273 let record = self.records.len();
274 self.records.resize(record + self.stride, 0);
275 let bytes = unsafe {
277 std::slice::from_raw_parts(
278 std::ptr::addr_of!(event).cast::<u8>(),
279 std::mem::size_of::<sys::mln_runtime_event>(),
280 )
281 };
282 self.records[record..record + bytes.len()].copy_from_slice(bytes);
283 self.count += 1;
284 }
285
286 pub(crate) fn raw(&self) -> sys::mln_runtime_event_batch {
287 sys::mln_runtime_event_batch {
288 size: std::mem::size_of::<sys::mln_runtime_event_batch>() as u32,
289 event_size: u32::try_from(self.stride).unwrap(),
290 events: self.records.as_ptr().cast(),
291 event_count: self.count,
292 messages: self.messages.as_ptr().cast(),
293 messages_size: self.messages.len(),
294 remaining_count: 0,
295 }
296 }
297
298 pub(crate) fn batch(&self) -> RuntimeEventBatch<'_> {
299 unsafe { RuntimeEventBatch::new(self.raw()) }
302 }
303}
304
305#[cfg(test)]
306mod tests {
307 use crate::ResourceErrorReason;
308
309 use super::*;
310
311 #[test]
312 fn a_batch_applies_the_rust_source_policy_to_every_source_kind() {
314 let mut batch = SynthesizedBatch::new();
315 let mut runtime_event =
316 SynthesizedBatch::zeroed_event(sys::MLN_RUNTIME_EVENT_OFFLINE_OPERATION_COMPLETED);
317 runtime_event.source_type = sys::MLN_RUNTIME_EVENT_SOURCE_RUNTIME;
318 batch.push(runtime_event, b"");
319 let mut map_event = SynthesizedBatch::zeroed_event(sys::MLN_RUNTIME_EVENT_MAP_STYLE_LOADED);
320 map_event.source_type = sys::MLN_RUNTIME_EVENT_SOURCE_MAP;
321 map_event.source = 0x0200_0000_0000_002a;
322 batch.push(map_event, b"");
323 let mut unknown_map = SynthesizedBatch::zeroed_event(sys::MLN_RUNTIME_EVENT_MAP_IDLE);
324 unknown_map.source_type = sys::MLN_RUNTIME_EVENT_SOURCE_MAP;
325 batch.push(unknown_map, b"");
326 let mut unknown_source = SynthesizedBatch::zeroed_event(sys::MLN_RUNTIME_EVENT_MAP_IDLE);
327 unknown_source.source_type = 999_003;
328 unknown_source.source = 0x0300_0000_0000_0063;
329 batch.push(unknown_source, b"");
330 let batch = batch.batch();
331
332 let sources = batch.iter().map(|event| event.source()).collect::<Vec<_>>();
333
334 assert_eq!(
335 sources,
336 vec![
337 RuntimeEventSource::Runtime,
338 RuntimeEventSource::Map(MapId::new(0x0200_0000_0000_002a)),
339 RuntimeEventSource::UnknownMap,
340 RuntimeEventSource::Unknown {
341 source_type: 999_003,
342 source: 0x0300_0000_0000_0063,
343 },
344 ]
345 );
346 }
347
348 #[test]
349 fn offline_events_decode_their_union_payloads_and_messages() {
351 let mut status = maplibre_core::events::empty_offline_region_status_native();
352 status.download_state = sys::MLN_OFFLINE_REGION_DOWNLOAD_ACTIVE;
353 status.completed_resource_count = 3;
354 status.complete = true;
355 let mut status_event =
356 SynthesizedBatch::zeroed_event(sys::MLN_RUNTIME_EVENT_OFFLINE_REGION_STATUS_CHANGED);
357 status_event.payload_type = sys::MLN_RUNTIME_EVENT_PAYLOAD_OFFLINE_REGION_STATUS;
358 status_event.payload.offline_region_status = sys::mln_runtime_event_offline_region_status {
359 region_id: 7,
360 status,
361 };
362 let mut error_event =
363 SynthesizedBatch::zeroed_event(sys::MLN_RUNTIME_EVENT_OFFLINE_REGION_RESPONSE_ERROR);
364 error_event.code = -1;
365 error_event.payload_type = sys::MLN_RUNTIME_EVENT_PAYLOAD_OFFLINE_REGION_RESPONSE_ERROR;
366 error_event.payload.offline_region_response_error =
367 sys::mln_runtime_event_offline_region_response_error {
368 region_id: 7,
369 reason: sys::MLN_RESOURCE_ERROR_REASON_OTHER,
370 };
371 let mut records = SynthesizedBatch::new();
372 records.push(status_event, b"");
373 records.push(error_event, b"offline failed");
374 let batch = records.batch();
375
376 let events = batch.iter().collect::<Vec<_>>();
377
378 assert_eq!(
379 events[0].event_type(),
380 RuntimeEventType::OfflineRegionStatusChanged
381 );
382 assert_eq!(events[0].source(), RuntimeEventSource::Runtime);
383 let RuntimeEventPayload::OfflineRegionStatus(status) = events[0].payload() else {
384 panic!("the first event should carry an offline region status payload");
385 };
386 assert_eq!(status.region_id, 7);
387 assert_eq!(
388 status.status.download_state,
389 OfflineRegionDownloadState::Active
390 );
391 assert_eq!(status.status.completed_resource_count, 3);
392 assert!(status.status.complete);
393
394 assert_eq!(events[1].message().unwrap(), Some("offline failed"));
395 assert_eq!(events[1].code(), -1);
396 let RuntimeEventPayload::OfflineRegionResponseError(error) = events[1].payload() else {
397 panic!("the second event should carry a response-error payload");
398 };
399 assert_eq!(error.region_id, 7);
400 assert_eq!(error.reason, ResourceErrorReason::Other);
401 }
402
403 #[test]
404 fn an_owned_event_copy_survives_the_storage_it_came_from() {
406 let mut records = SynthesizedBatch::new();
407 let mut event = SynthesizedBatch::zeroed_event(999_001);
408 event.source_type = sys::MLN_RUNTIME_EVENT_SOURCE_MAP;
409 event.source = 42;
410 event.code = -7;
411 event.payload_type = 999_002;
412 event.payload.camera_transition_finished =
413 sys::mln_runtime_event_camera_transition_finished {
414 transition_id: 0x0102_0304_0506_0708,
415 };
416 records.push(event, b"future payload");
417 let batch = records.batch();
418 let borrowed = batch.iter().next().unwrap();
419
420 let owned = borrowed.to_owned().unwrap();
421 assert_eq!(borrowed.message_bytes(), b"future payload");
422 drop(records);
425
426 assert_eq!(owned.event_type, RuntimeEventType::Unknown(999_001));
427 assert_eq!(owned.source, RuntimeEventSource::Map(MapId::new(42)));
428 assert_eq!(owned.code, -7);
429 assert_eq!(owned.message.as_deref(), Some("future payload"));
430 let RuntimeEventPayload::Unknown(payload) = &owned.payload else {
431 panic!("an undefined payload type should stay opaque");
432 };
433 assert_eq!(payload.raw_type, 999_002);
434 assert_eq!(
435 &payload.bytes[..8],
436 &0x0102_0304_0506_0708_u64.to_ne_bytes()
437 );
438 }
439
440 #[test]
441 fn a_message_that_is_not_utf8_fails_only_its_own_event() {
442 let mut records = SynthesizedBatch::new();
443 records.push(
444 SynthesizedBatch::zeroed_event(sys::MLN_RUNTIME_EVENT_MAP_LOADING_FAILED),
445 &[0xff, 0xfe],
446 );
447 records.push(
448 SynthesizedBatch::zeroed_event(sys::MLN_RUNTIME_EVENT_MAP_STYLE_LOADED),
449 b"loaded",
450 );
451 let batch = records.batch();
452 let events = batch.iter().collect::<Vec<_>>();
453
454 let error = events[0].message().unwrap_err();
455 assert_eq!(error.kind(), crate::ErrorKind::InvalidArgument);
456 assert!(error.diagnostic().contains("not valid UTF-8"));
457 assert_eq!(events[0].message_bytes(), &[0xff, 0xfe]);
458 assert!(events[0].to_owned().is_err());
459 assert_eq!(events[1].message().unwrap(), Some("loaded"));
460 assert!(events[1].to_owned().is_ok());
461 }
462}