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::{ConcurrentNativeHandle, 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: ConcurrentNativeHandle<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            ConcurrentNativeHandle::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/// usable from any thread, and native calls serialize access to its transform.
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    ///
162    /// The longitude is wrapped to the range from -180 to 180 degrees.
163    pub fn lat_lng_for_pixel(&self, point: ScreenPoint) -> Result<LatLng> {
164        let projection = self.inner.native()?;
165        let mut raw_coordinate = empty_lat_lng();
166        // SAFETY: projection is live, point is passed by value, and
167        // raw_coordinate is writable output storage.
168        maplibre_core::check(unsafe {
169            sys::mln_map_projection_lat_lng_for_pixel(
170                projection,
171                point.to_native(),
172                &mut raw_coordinate,
173            )
174        })?;
175        Ok(LatLng::from_native(raw_coordinate))
176    }
177
178    /// Converts a screen point to an unwrapped geographic coordinate.
179    ///
180    /// The longitude preserves the visible world copy and may fall outside
181    /// -180 to 180.
182    pub fn lat_lng_for_pixel_unwrapped(&self, point: ScreenPoint) -> Result<LatLng> {
183        let projection = self.inner.native()?;
184        let mut raw_coordinate = empty_lat_lng();
185        // SAFETY: projection is live, point is passed by value, and
186        // raw_coordinate is writable output storage.
187        maplibre_core::check(unsafe {
188            sys::mln_map_projection_lat_lng_for_pixel_unwrapped(
189                projection,
190                point.to_native(),
191                &mut raw_coordinate,
192            )
193        })?;
194        Ok(LatLng::from_native(raw_coordinate))
195    }
196}
197
198#[cfg(test)]
199mod tests {
200    use static_assertions::assert_impl_all;
201
202    use super::*;
203    use crate::{ErrorKind, MapOptions, RuntimeHandle};
204
205    assert_impl_all!(MapProjectionHandle: Send, Sync);
206
207    #[test]
208    // Spec coverage: BND-043 and BND-103.
209    fn projection_create_round_trip_close_and_stays_live_after_map_close() {
210        let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
211        let map = MapHandle::with_options(&runtime, &MapOptions::new(512, 512, 1.0)).unwrap();
212        let center = LatLng::new(37.7749, -122.4194);
213        let mut camera_options = CameraOptions::default();
214        camera_options.center = Some(center);
215        camera_options.zoom = Some(5.0);
216        map.jump_to(&camera_options).unwrap();
217
218        let projection = map.create_projection().unwrap();
219        map.close().unwrap();
220        runtime.close().unwrap();
221
222        std::thread::spawn(move || {
223            let point = projection.pixel_for_lat_lng(center).unwrap();
224            let round_tripped = projection.lat_lng_for_pixel(point).unwrap();
225            assert!((round_tripped.latitude - center.latitude).abs() < 1e-7);
226            assert!((round_tripped.longitude - center.longitude).abs() < 1e-7);
227            projection.close().unwrap();
228        })
229        .join()
230        .unwrap();
231    }
232
233    #[test]
234    // Rust regression: dropping a projection without explicit close must not
235    // attempt unsafe cleanup from an uncontrolled destructor path.
236    fn projection_drops_without_explicit_close() {
237        let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
238        let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap();
239
240        {
241            let _projection = map.create_projection().unwrap();
242        }
243
244        map.close().unwrap();
245        runtime.close().unwrap();
246    }
247
248    #[test]
249    // Spec coverage: BND-103.
250    fn projection_camera_and_visible_region_helpers_call_c_api() {
251        let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
252        let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap();
253        let projection = map.create_projection().unwrap();
254
255        let mut camera_options = CameraOptions::default();
256        camera_options.center = Some(LatLng::new(0.0, 0.0));
257        camera_options.zoom = Some(2.0);
258        projection.set_camera(&camera_options).unwrap();
259        let camera = projection.camera().unwrap();
260        assert_eq!(camera.center, Some(LatLng::new(0.0, 0.0)));
261        assert_eq!(camera.zoom, Some(2.0));
262
263        let padding = EdgeInsets::new(0.0, 0.0, 0.0, 0.0);
264        projection
265            .set_visible_coordinates(&[LatLng::new(0.0, 0.0), LatLng::new(1.0, 1.0)], padding)
266            .unwrap();
267        let error = projection
268            .set_visible_coordinates(&[], padding)
269            .unwrap_err();
270        assert_eq!(error.kind(), ErrorKind::InvalidArgument);
271        assert_eq!(error.raw_status(), None);
272        assert!(error.diagnostic().contains("at least one coordinate"));
273        projection
274            .set_visible_geometry(
275                br#"{"type":"LineString","coordinates":[[0.0,0.0],[1.0,1.0]]}"#,
276                padding,
277            )
278            .unwrap();
279
280        projection.close().unwrap();
281        map.close().unwrap();
282        runtime.close().unwrap();
283    }
284}