Skip to main content

maplibre_native_ffi/
projection.rs

1use std::fmt;
2
3use maplibre_native_ffi_core as maplibre_core;
4use maplibre_native_ffi_core::ptr::const_ptr_or_null;
5use maplibre_native_ffi_core::values::{empty_lat_lng, empty_screen_point, lat_lngs_to_native};
6use maplibre_native_ffi_sys as sys;
7
8use crate::camera::CameraOptionsNativeExt;
9use crate::handle::{ThreadAffineNativeHandle, closed_handle_error, out_handle};
10use crate::values::NativeValue;
11use crate::{
12    CameraOptions, EdgeInsets, Error, HandleOperationError, LatLng, MapHandle, Result, ScreenPoint,
13};
14
15#[derive(Debug)]
16pub(crate) struct MapProjectionState {
17    handle: ThreadAffineNativeHandle<sys::mln_map_projection>,
18}
19
20impl MapProjectionState {
21    fn new(native: sys::mln_map_projection) -> Result<Self> {
22        // SAFETY: native came from successful mln_map_projection_create and is
23        // paired with the matching projection destroy function.
24        let handle = unsafe {
25            ThreadAffineNativeHandle::from_handle(
26                native,
27                sys::mln_map_projection_destroy,
28                "mln_map_projection",
29            )
30        }?;
31        Ok(Self { handle })
32    }
33
34    fn native(&self) -> Result<sys::mln_map_projection> {
35        self.handle
36            .live_handle()
37            .ok_or_else(|| closed_handle_error("MapProjectionHandle"))
38    }
39
40    fn is_closed(&self) -> bool {
41        self.handle.is_closed()
42    }
43
44    fn close(&self) -> Result<()> {
45        self.handle.close()
46    }
47}
48
49/// Standalone projection snapshot created from a map transform.
50///
51/// The projection does not retain the source map after creation. It remains
52/// thread-affine and must be used and closed on its owner thread.
53pub struct MapProjectionHandle {
54    inner: MapProjectionState,
55}
56
57impl fmt::Debug for MapProjectionHandle {
58    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
59        f.debug_struct("MapProjectionHandle")
60            .field("closed", &self.inner.is_closed())
61            .finish()
62    }
63}
64
65impl MapProjectionHandle {
66    pub(crate) fn new(map: &MapHandle) -> Result<Self> {
67        let map_ptr = map.inner.native()?;
68        let mut out = maplibre_core::ptr::OutHandle::<sys::mln_map_projection>::new();
69        // SAFETY: map_ptr is a live map handle. out is a valid null-initialized
70        // out-pointer owned by this call.
71        maplibre_core::check(unsafe { sys::mln_map_projection_create(map_ptr, out.as_mut_ptr()) })?;
72        let ptr = out_handle(out, "mln_map_projection")?;
73        Ok(Self {
74            inner: MapProjectionState::new(ptr)?,
75        })
76    }
77
78    /// Explicitly destroys the projection snapshot.
79    pub fn close(self) -> std::result::Result<(), HandleOperationError<Self>> {
80        self.inner
81            .close()
82            .map_err(|error| HandleOperationError::new(error, self))
83    }
84
85    /// Reads the projection helper's current camera snapshot.
86    pub fn camera(&self) -> Result<CameraOptions> {
87        let projection = self.inner.native()?;
88        // SAFETY: Default constructor takes no arguments and initializes size.
89        let mut raw = unsafe { sys::mln_camera_options_default() };
90        // SAFETY: projection is live and raw has a valid size field for C to fill.
91        maplibre_core::check(unsafe { sys::mln_map_projection_get_camera(projection, &mut raw) })?;
92        Ok(CameraOptions::from_native(raw))
93    }
94
95    /// Applies camera fields to this projection helper.
96    pub fn set_camera(&self, camera: &CameraOptions) -> Result<()> {
97        let projection = self.inner.native()?;
98        let raw = camera.to_native();
99        // SAFETY: projection is live and raw is a materialized descriptor valid
100        // for the duration of this call.
101        maplibre_core::check(unsafe { sys::mln_map_projection_set_camera(projection, &raw) })
102    }
103
104    /// Updates the projection camera so coordinates are visible within padding.
105    pub fn set_visible_coordinates(
106        &self,
107        coordinates: &[LatLng],
108        padding: EdgeInsets,
109    ) -> Result<()> {
110        let projection = self.inner.native()?;
111        if coordinates.is_empty() {
112            return Err(Error::invalid_argument(
113                "set_visible_coordinates requires at least one coordinate",
114            ));
115        }
116        let raw_coordinates = lat_lngs_to_native(coordinates);
117        // SAFETY: projection is live. coordinates points to coordinate_count
118        // non-empty entries. padding is passed by value.
119        maplibre_core::check(unsafe {
120            sys::mln_map_projection_set_visible_coordinates(
121                projection,
122                const_ptr_or_null(&raw_coordinates),
123                raw_coordinates.len(),
124                padding.to_native(),
125            )
126        })
127    }
128
129    /// Updates the projection camera so geometry coordinates are visible.
130    pub fn set_visible_geometry(&self, geometry: &[u8], padding: EdgeInsets) -> Result<()> {
131        let projection = self.inner.native()?;
132        let native_geometry = maplibre_core::string::buffer_view(geometry);
133        // SAFETY: projection is live, native_geometry owns backing storage for
134        // the duration of this call, and padding is passed by value.
135        maplibre_core::check(unsafe {
136            sys::mln_map_projection_set_visible_geometry(
137                projection,
138                native_geometry,
139                padding.to_native(),
140            )
141        })
142    }
143
144    /// Converts a geographic world coordinate to a screen point.
145    pub fn pixel_for_lat_lng(&self, coordinate: LatLng) -> Result<ScreenPoint> {
146        let projection = self.inner.native()?;
147        let mut raw_point = empty_screen_point();
148        // SAFETY: projection is live, coordinate is passed by value, and
149        // raw_point is writable output storage.
150        maplibre_core::check(unsafe {
151            sys::mln_map_projection_pixel_for_lat_lng(
152                projection,
153                coordinate.to_native(),
154                &mut raw_point,
155            )
156        })?;
157        Ok(ScreenPoint::from_native(raw_point))
158    }
159
160    /// Converts a screen point to a geographic world coordinate.
161    pub fn lat_lng_for_pixel(&self, point: ScreenPoint) -> Result<LatLng> {
162        let projection = self.inner.native()?;
163        let mut raw_coordinate = empty_lat_lng();
164        // SAFETY: projection is live, point is passed by value, and
165        // raw_coordinate is writable output storage.
166        maplibre_core::check(unsafe {
167            sys::mln_map_projection_lat_lng_for_pixel(
168                projection,
169                point.to_native(),
170                &mut raw_coordinate,
171            )
172        })?;
173        Ok(LatLng::from_native(raw_coordinate))
174    }
175}
176
177#[cfg(test)]
178mod tests {
179    use static_assertions::assert_not_impl_any;
180
181    use super::*;
182    use crate::{ErrorKind, MapOptions, RuntimeHandle};
183
184    assert_not_impl_any!(MapProjectionHandle: Send, Sync);
185
186    #[test]
187    // Spec coverage: BND-043 and BND-103.
188    fn projection_create_round_trip_close_and_stays_live_after_map_close() {
189        let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
190        let map = MapHandle::with_options(&runtime, &MapOptions::new(512, 512, 1.0)).unwrap();
191        let center = LatLng::new(37.7749, -122.4194);
192        let mut camera_options = CameraOptions::default();
193        camera_options.center = Some(center);
194        camera_options.zoom = Some(5.0);
195        map.jump_to(&camera_options).unwrap();
196
197        let projection = map.create_projection().unwrap();
198        map.close().unwrap();
199        runtime.close().unwrap();
200
201        let point = projection.pixel_for_lat_lng(center).unwrap();
202        let round_tripped = projection.lat_lng_for_pixel(point).unwrap();
203        assert!((round_tripped.latitude - center.latitude).abs() < 1e-7);
204        assert!((round_tripped.longitude - center.longitude).abs() < 1e-7);
205
206        projection.close().unwrap();
207    }
208
209    #[test]
210    // Rust regression: dropping a projection without explicit close must not
211    // attempt unsafe cleanup from an uncontrolled destructor path.
212    fn projection_drops_without_explicit_close() {
213        let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
214        let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap();
215
216        {
217            let _projection = map.create_projection().unwrap();
218        }
219
220        map.close().unwrap();
221        runtime.close().unwrap();
222    }
223
224    #[test]
225    // Spec coverage: BND-103.
226    fn projection_camera_and_visible_region_helpers_call_c_api() {
227        let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
228        let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap();
229        let projection = map.create_projection().unwrap();
230
231        let mut camera_options = CameraOptions::default();
232        camera_options.center = Some(LatLng::new(0.0, 0.0));
233        camera_options.zoom = Some(2.0);
234        projection.set_camera(&camera_options).unwrap();
235        let camera = projection.camera().unwrap();
236        assert_eq!(camera.center, Some(LatLng::new(0.0, 0.0)));
237        assert_eq!(camera.zoom, Some(2.0));
238
239        let padding = EdgeInsets::new(0.0, 0.0, 0.0, 0.0);
240        projection
241            .set_visible_coordinates(&[LatLng::new(0.0, 0.0), LatLng::new(1.0, 1.0)], padding)
242            .unwrap();
243        let error = projection
244            .set_visible_coordinates(&[], padding)
245            .unwrap_err();
246        assert_eq!(error.kind(), ErrorKind::InvalidArgument);
247        assert_eq!(error.raw_status(), None);
248        assert!(error.diagnostic().contains("at least one coordinate"));
249        projection
250            .set_visible_geometry(
251                br#"{"type":"LineString","coordinates":[[0.0,0.0],[1.0,1.0]]}"#,
252                padding,
253            )
254            .unwrap();
255
256        projection.close().unwrap();
257        map.close().unwrap();
258        runtime.close().unwrap();
259    }
260}