Skip to main content

maplibre_native/
custom_geometry.rs

1use std::fmt;
2use std::os::raw::c_void;
3use std::panic::{AssertUnwindSafe, catch_unwind};
4use std::ptr;
5use std::sync::{Condvar, Mutex};
6
7use maplibre_native_core as maplibre_core;
8use maplibre_native_sys as sys;
9
10/// Canonical tile identity used by custom geometry source callbacks.
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
12#[non_exhaustive]
13pub struct CanonicalTileId {
14    pub z: u32,
15    pub x: u32,
16    pub y: u32,
17}
18
19impl CanonicalTileId {
20    pub const fn new(z: u32, x: u32, y: u32) -> Self {
21        Self { z, x, y }
22    }
23
24    pub(crate) fn from_native(raw: sys::mln_canonical_tile_id) -> Self {
25        Self {
26            z: raw.z,
27            x: raw.x,
28            y: raw.y,
29        }
30    }
31
32    pub(crate) fn to_native(self) -> sys::mln_canonical_tile_id {
33        sys::mln_canonical_tile_id {
34            z: self.z,
35            x: self.x,
36            y: self.y,
37        }
38    }
39}
40
41type TileCallback = dyn Fn(CanonicalTileId) + Send + Sync + 'static;
42
43/// Options used when adding a custom geometry source.
44///
45/// Custom geometry callbacks may run on native worker threads. Keep callbacks
46/// quick, and hand work back to the map owner thread before calling map APIs
47/// such as `set_custom_geometry_source_tile_data` or invalidation helpers.
48#[non_exhaustive]
49pub struct CustomGeometrySourceOptions {
50    fetch_tile: Box<TileCallback>,
51    cancel_tile: Option<Box<TileCallback>>,
52    /// Minimum zoom level at which the source produces tiles.
53    pub min_zoom: Option<f64>,
54    /// Maximum zoom level at which the source produces tiles.
55    pub max_zoom: Option<f64>,
56    /// Douglas-Peucker simplification tolerance in tile coordinate units.
57    pub tolerance: Option<f64>,
58    /// Tile extent in pixels, usually 512.
59    pub tile_size: Option<u32>,
60    /// Extra tile buffer in pixels for geometry that crosses tile edges.
61    pub buffer: Option<u32>,
62    /// Whether native clips geometries to tile bounds.
63    pub clip: Option<bool>,
64    /// Whether the source wraps horizontally across the antimeridian.
65    pub wrap: Option<bool>,
66}
67
68impl fmt::Debug for CustomGeometrySourceOptions {
69    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
70        f.debug_struct("CustomGeometrySourceOptions")
71            .field("has_cancel_tile", &self.cancel_tile.is_some())
72            .field("min_zoom", &self.min_zoom)
73            .field("max_zoom", &self.max_zoom)
74            .field("tolerance", &self.tolerance)
75            .field("tile_size", &self.tile_size)
76            .field("buffer", &self.buffer)
77            .field("clip", &self.clip)
78            .field("wrap", &self.wrap)
79            .finish_non_exhaustive()
80    }
81}
82
83impl CustomGeometrySourceOptions {
84    pub fn new<F>(fetch_tile: F) -> Self
85    where
86        F: Fn(CanonicalTileId) + Send + Sync + 'static,
87    {
88        Self {
89            fetch_tile: Box::new(fetch_tile),
90            cancel_tile: None,
91            min_zoom: None,
92            max_zoom: None,
93            tolerance: None,
94            tile_size: None,
95            buffer: None,
96            clip: None,
97            wrap: None,
98        }
99    }
100
101    pub fn with_cancel_tile<F>(mut self, cancel_tile: F) -> Self
102    where
103        F: Fn(CanonicalTileId) + Send + Sync + 'static,
104    {
105        self.cancel_tile = Some(Box::new(cancel_tile));
106        self
107    }
108
109    pub fn with_min_zoom(mut self, min_zoom: f64) -> Self {
110        self.min_zoom = Some(min_zoom);
111        self
112    }
113
114    pub fn with_max_zoom(mut self, max_zoom: f64) -> Self {
115        self.max_zoom = Some(max_zoom);
116        self
117    }
118
119    pub fn with_tolerance(mut self, tolerance: f64) -> Self {
120        self.tolerance = Some(tolerance);
121        self
122    }
123
124    pub fn with_tile_size(mut self, tile_size: u32) -> Self {
125        self.tile_size = Some(tile_size);
126        self
127    }
128
129    pub fn with_buffer(mut self, buffer: u32) -> Self {
130        self.buffer = Some(buffer);
131        self
132    }
133
134    pub fn with_clip(mut self, clip: bool) -> Self {
135        self.clip = Some(clip);
136        self
137    }
138
139    pub fn with_wrap(mut self, wrap: bool) -> Self {
140        self.wrap = Some(wrap);
141        self
142    }
143}
144
145#[derive(Debug, Default)]
146struct CallbackLifecycle {
147    active: usize,
148    closing: bool,
149    closed: bool,
150}
151
152pub(crate) struct CustomGeometrySourceState {
153    fetch_tile: Box<TileCallback>,
154    cancel_tile: Option<Box<TileCallback>>,
155    min_zoom: Option<f64>,
156    max_zoom: Option<f64>,
157    tolerance: Option<f64>,
158    tile_size: Option<u32>,
159    buffer: Option<u32>,
160    clip: Option<bool>,
161    wrap: Option<bool>,
162    lifecycle: Mutex<CallbackLifecycle>,
163    idle: Condvar,
164}
165
166impl fmt::Debug for CustomGeometrySourceState {
167    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
168        f.debug_struct("CustomGeometrySourceState")
169            .finish_non_exhaustive()
170    }
171}
172
173impl CustomGeometrySourceState {
174    pub(crate) fn new(options: CustomGeometrySourceOptions) -> Box<Self> {
175        Box::new(Self {
176            fetch_tile: options.fetch_tile,
177            cancel_tile: options.cancel_tile,
178            min_zoom: options.min_zoom,
179            max_zoom: options.max_zoom,
180            tolerance: options.tolerance,
181            tile_size: options.tile_size,
182            buffer: options.buffer,
183            clip: options.clip,
184            wrap: options.wrap,
185            lifecycle: Mutex::new(CallbackLifecycle::default()),
186            idle: Condvar::new(),
187        })
188    }
189
190    pub(crate) fn descriptor(&self) -> sys::mln_custom_geometry_source_options {
191        maplibre_core::style::custom_geometry_source_options_to_native(
192            maplibre_core::style::CustomGeometrySourceDescriptorFields {
193                fetch_tile: Some(fetch_tile_trampoline),
194                cancel_tile: self
195                    .cancel_tile
196                    .as_ref()
197                    .map(|_| cancel_tile_trampoline as _),
198                user_data: ptr::from_ref(self).cast_mut().cast::<c_void>(),
199                min_zoom: self.min_zoom,
200                max_zoom: self.max_zoom,
201                tolerance: self.tolerance,
202                tile_size: self.tile_size,
203                buffer: self.buffer,
204                clip: self.clip,
205                wrap: self.wrap,
206            },
207        )
208    }
209
210    pub(crate) fn close(&self) {
211        let mut lifecycle = self
212            .lifecycle
213            .lock()
214            .unwrap_or_else(|poisoned| poisoned.into_inner());
215        if lifecycle.closed {
216            return;
217        }
218        lifecycle.closing = true;
219        while lifecycle.active != 0 {
220            lifecycle = self
221                .idle
222                .wait(lifecycle)
223                .unwrap_or_else(|poisoned| poisoned.into_inner());
224        }
225        lifecycle.closed = true;
226    }
227
228    fn invoke_fetch(&self, tile_id: CanonicalTileId) {
229        let Some(_guard) = self.enter_callback() else {
230            return;
231        };
232        let _ = catch_unwind(AssertUnwindSafe(|| (self.fetch_tile)(tile_id)));
233    }
234
235    fn invoke_cancel(&self, tile_id: CanonicalTileId) {
236        let Some(_guard) = self.enter_callback() else {
237            return;
238        };
239        if let Some(cancel_tile) = &self.cancel_tile {
240            let _ = catch_unwind(AssertUnwindSafe(|| cancel_tile(tile_id)));
241        }
242    }
243
244    fn enter_callback(&self) -> Option<CallbackGuard<'_>> {
245        let mut lifecycle = self
246            .lifecycle
247            .lock()
248            .unwrap_or_else(|poisoned| poisoned.into_inner());
249        if lifecycle.closing || lifecycle.closed {
250            return None;
251        }
252        lifecycle.active += 1;
253        Some(CallbackGuard { state: self })
254    }
255
256    fn exit_callback(&self) {
257        let mut lifecycle = self
258            .lifecycle
259            .lock()
260            .unwrap_or_else(|poisoned| poisoned.into_inner());
261        lifecycle.active -= 1;
262        if lifecycle.active == 0 {
263            self.idle.notify_all();
264        }
265    }
266}
267
268impl Drop for CustomGeometrySourceState {
269    fn drop(&mut self) {
270        self.close();
271    }
272}
273
274struct CallbackGuard<'a> {
275    state: &'a CustomGeometrySourceState,
276}
277
278impl Drop for CallbackGuard<'_> {
279    fn drop(&mut self) {
280        self.state.exit_callback();
281    }
282}
283
284unsafe extern "C" fn fetch_tile_trampoline(
285    user_data: *mut c_void,
286    tile_id: sys::mln_canonical_tile_id,
287) {
288    let Some(state) = ptr::NonNull::new(user_data.cast::<CustomGeometrySourceState>()) else {
289        return;
290    };
291    // SAFETY: user_data is installed from CustomGeometrySourceState::descriptor
292    // and remains valid until source/style/map teardown waits for in-flight callbacks.
293    unsafe { state.as_ref() }.invoke_fetch(CanonicalTileId::from_native(tile_id));
294}
295
296unsafe extern "C" fn cancel_tile_trampoline(
297    user_data: *mut c_void,
298    tile_id: sys::mln_canonical_tile_id,
299) {
300    let Some(state) = ptr::NonNull::new(user_data.cast::<CustomGeometrySourceState>()) else {
301        return;
302    };
303    // SAFETY: user_data is installed from CustomGeometrySourceState::descriptor
304    // and remains valid until source/style/map teardown waits for in-flight callbacks.
305    unsafe { state.as_ref() }.invoke_cancel(CanonicalTileId::from_native(tile_id));
306}
307
308#[cfg(test)]
309mod tests {
310    use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
311    use std::sync::{Arc, Condvar, Mutex};
312    use std::time::Duration;
313
314    use super::*;
315
316    fn tile(z: u32, x: u32, y: u32) -> sys::mln_canonical_tile_id {
317        CanonicalTileId::new(z, x, y).to_native()
318    }
319
320    #[test]
321    fn custom_geometry_callbacks_invoke_fetch_and_cancel_with_copied_tile_id() {
322        let fetched = Arc::new(Mutex::new(Vec::new()));
323        let cancelled = Arc::new(Mutex::new(Vec::new()));
324        let fetched_callback = Arc::clone(&fetched);
325        let cancelled_callback = Arc::clone(&cancelled);
326        let state = CustomGeometrySourceState::new(
327            CustomGeometrySourceOptions::new(move |tile_id| {
328                fetched_callback.lock().unwrap().push(tile_id);
329            })
330            .with_cancel_tile(move |tile_id| {
331                cancelled_callback.lock().unwrap().push(tile_id);
332            }),
333        );
334        let descriptor = state.descriptor();
335
336        unsafe {
337            descriptor.fetch_tile.unwrap()(descriptor.user_data, tile(1, 2, 3));
338            descriptor.cancel_tile.unwrap()(descriptor.user_data, tile(4, 5, 6));
339        }
340
341        assert_eq!(
342            fetched.lock().unwrap().as_slice(),
343            &[CanonicalTileId::new(1, 2, 3)]
344        );
345        assert_eq!(
346            cancelled.lock().unwrap().as_slice(),
347            &[CanonicalTileId::new(4, 5, 6)]
348        );
349    }
350
351    #[test]
352    fn custom_geometry_callbacks_contain_panics() {
353        let cancel_called = Arc::new(AtomicBool::new(false));
354        let cancel_called_callback = Arc::clone(&cancel_called);
355        let state = CustomGeometrySourceState::new(
356            CustomGeometrySourceOptions::new(|_| panic!("fetch panic")).with_cancel_tile(
357                move |_| {
358                    cancel_called_callback.store(true, Ordering::SeqCst);
359                    panic!("cancel panic");
360                },
361            ),
362        );
363        let descriptor = state.descriptor();
364
365        unsafe {
366            descriptor.fetch_tile.unwrap()(descriptor.user_data, tile(0, 0, 0));
367            descriptor.cancel_tile.unwrap()(descriptor.user_data, tile(0, 0, 0));
368        }
369
370        assert!(cancel_called.load(Ordering::SeqCst));
371    }
372
373    #[test]
374    fn custom_geometry_state_release_waits_for_active_upcalls() {
375        let entered = Arc::new((Mutex::new(false), Condvar::new()));
376        let release = Arc::new((Mutex::new(false), Condvar::new()));
377        let closed = Arc::new(AtomicBool::new(false));
378        let close_attempts = Arc::new(AtomicUsize::new(0));
379        let entered_callback = Arc::clone(&entered);
380        let release_callback = Arc::clone(&release);
381        let state = CustomGeometrySourceState::new(CustomGeometrySourceOptions::new(move |_| {
382            let (entered_lock, entered_cvar) = &*entered_callback;
383            *entered_lock.lock().unwrap() = true;
384            entered_cvar.notify_all();
385
386            let (release_lock, release_cvar) = &*release_callback;
387            let released = release_lock.lock().unwrap();
388            let (_released, timeout) = release_cvar
389                .wait_timeout_while(released, Duration::from_secs(5), |released| !*released)
390                .unwrap();
391            assert!(!timeout.timed_out());
392        }));
393        let descriptor = state.descriptor();
394        let callback = descriptor.fetch_tile.unwrap();
395        let user_data = descriptor.user_data as usize;
396
397        std::thread::scope(|scope| {
398            scope.spawn(move || unsafe {
399                callback(user_data as *mut c_void, tile(1, 1, 1));
400            });
401            let (entered_lock, entered_cvar) = &*entered;
402            let entered_guard = entered_lock.lock().unwrap();
403            let (_entered_guard, timeout) = entered_cvar
404                .wait_timeout_while(entered_guard, Duration::from_secs(5), |entered| !*entered)
405                .unwrap();
406            assert!(!timeout.timed_out());
407
408            let closed_for_thread = Arc::clone(&closed);
409            let close_attempts_for_thread = Arc::clone(&close_attempts);
410            let state_ref = &*state;
411            scope.spawn(move || {
412                close_attempts_for_thread.fetch_add(1, Ordering::SeqCst);
413                state_ref.close();
414                closed_for_thread.store(true, Ordering::SeqCst);
415            });
416
417            std::thread::sleep(Duration::from_millis(50));
418            assert_eq!(close_attempts.load(Ordering::SeqCst), 1);
419            assert!(!closed.load(Ordering::SeqCst));
420            let (release_lock, release_cvar) = &*release;
421            *release_lock.lock().unwrap() = true;
422            release_cvar.notify_all();
423        });
424
425        assert!(closed.load(Ordering::SeqCst));
426        unsafe {
427            descriptor.fetch_tile.unwrap()(descriptor.user_data, tile(9, 9, 9));
428        }
429    }
430}