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#[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#[non_exhaustive]
49pub struct CustomGeometrySourceOptions {
50 fetch_tile: Box<TileCallback>,
51 cancel_tile: Option<Box<TileCallback>>,
52 pub min_zoom: Option<f64>,
54 pub max_zoom: Option<f64>,
56 pub tolerance: Option<f64>,
58 pub tile_size: Option<u32>,
60 pub buffer: Option<u32>,
62 pub clip: Option<bool>,
64 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 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 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}