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 projection creation 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 its source 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        Self::from_native(ptr)
74    }
75
76    pub(crate) fn from_native(ptr: sys::mln_map_projection) -> Result<Self> {
77        Ok(Self {
78            inner: MapProjectionState::new(ptr)?,
79        })
80    }
81
82    /// Explicitly destroys the projection snapshot.
83    pub fn close(self) -> std::result::Result<(), HandleOperationError<Self>> {
84        self.inner
85            .close()
86            .map_err(|error| HandleOperationError::new(error, self))
87    }
88
89    /// Reads the projection helper's current camera snapshot.
90    pub fn camera(&self) -> Result<CameraOptions> {
91        let projection = self.inner.native()?;
92        // SAFETY: Default constructor takes no arguments and initializes size.
93        let mut raw = unsafe { sys::mln_camera_options_default() };
94        // SAFETY: projection is live and raw has a valid size field for C to fill.
95        maplibre_core::check(unsafe { sys::mln_map_projection_get_camera(projection, &mut raw) })?;
96        Ok(CameraOptions::from_native(raw))
97    }
98
99    /// Applies camera fields to this projection helper.
100    pub fn set_camera(&self, camera: &CameraOptions) -> Result<()> {
101        let projection = self.inner.native()?;
102        let raw = camera.to_native();
103        // SAFETY: projection is live and raw is a materialized descriptor valid
104        // for the duration of this call.
105        maplibre_core::check(unsafe { sys::mln_map_projection_set_camera(projection, &raw) })
106    }
107
108    /// Updates the projection camera so coordinates are visible within padding.
109    pub fn set_visible_coordinates(
110        &self,
111        coordinates: &[LatLng],
112        padding: EdgeInsets,
113    ) -> Result<()> {
114        let projection = self.inner.native()?;
115        if coordinates.is_empty() {
116            return Err(Error::invalid_argument(
117                "set_visible_coordinates requires at least one coordinate",
118            ));
119        }
120        let raw_coordinates = lat_lngs_to_native(coordinates);
121        // SAFETY: projection is live. coordinates points to coordinate_count
122        // non-empty entries. padding is passed by value.
123        maplibre_core::check(unsafe {
124            sys::mln_map_projection_set_visible_coordinates(
125                projection,
126                const_ptr_or_null(&raw_coordinates),
127                raw_coordinates.len(),
128                padding.to_native(),
129            )
130        })
131    }
132
133    /// Updates the projection camera so geometry coordinates are visible.
134    pub fn set_visible_geometry(&self, geometry: &[u8], padding: EdgeInsets) -> Result<()> {
135        let projection = self.inner.native()?;
136        let native_geometry = maplibre_core::string::buffer_view(geometry);
137        // SAFETY: projection is live, native_geometry owns backing storage for
138        // the duration of this call, and padding is passed by value.
139        maplibre_core::check(unsafe {
140            sys::mln_map_projection_set_visible_geometry(
141                projection,
142                native_geometry,
143                padding.to_native(),
144            )
145        })
146    }
147
148    /// Converts a geographic world coordinate to a screen point.
149    pub fn pixel_for_lat_lng(&self, coordinate: LatLng) -> Result<ScreenPoint> {
150        let projection = self.inner.native()?;
151        let mut raw_point = empty_screen_point();
152        // SAFETY: projection is live, coordinate is passed by value, and
153        // raw_point is writable output storage.
154        maplibre_core::check(unsafe {
155            sys::mln_map_projection_pixel_for_lat_lng(
156                projection,
157                coordinate.to_native(),
158                &mut raw_point,
159            )
160        })?;
161        Ok(ScreenPoint::from_native(raw_point))
162    }
163
164    /// Converts a screen point to a geographic world coordinate.
165    ///
166    /// The longitude is wrapped to the range from -180 to 180 degrees.
167    pub fn lat_lng_for_pixel(&self, point: ScreenPoint) -> Result<LatLng> {
168        let projection = self.inner.native()?;
169        let mut raw_coordinate = empty_lat_lng();
170        // SAFETY: projection is live, point is passed by value, and
171        // raw_coordinate is writable output storage.
172        maplibre_core::check(unsafe {
173            sys::mln_map_projection_lat_lng_for_pixel(
174                projection,
175                point.to_native(),
176                &mut raw_coordinate,
177            )
178        })?;
179        Ok(LatLng::from_native(raw_coordinate))
180    }
181
182    /// Converts a screen point to an unwrapped geographic coordinate.
183    ///
184    /// The longitude preserves the visible world copy and may fall outside
185    /// -180 to 180.
186    pub fn lat_lng_for_pixel_unwrapped(&self, point: ScreenPoint) -> Result<LatLng> {
187        let projection = self.inner.native()?;
188        let mut raw_coordinate = empty_lat_lng();
189        // SAFETY: projection is live, point is passed by value, and
190        // raw_coordinate is writable output storage.
191        maplibre_core::check(unsafe {
192            sys::mln_map_projection_lat_lng_for_pixel_unwrapped(
193                projection,
194                point.to_native(),
195                &mut raw_coordinate,
196            )
197        })?;
198        Ok(LatLng::from_native(raw_coordinate))
199    }
200
201    /// Reads the ground distance in meters covered by one logical map pixel at
202    /// a latitude for the helper camera zoom.
203    pub fn meters_per_pixel_at_latitude(&self, latitude: f64) -> Result<f64> {
204        let projection = self.inner.native()?;
205        let mut meters_per_pixel = 0.0;
206        // SAFETY: projection is live and meters_per_pixel is writable output
207        // storage.
208        maplibre_core::check(unsafe {
209            sys::mln_map_projection_meters_per_pixel_at_latitude(
210                projection,
211                latitude,
212                &mut meters_per_pixel,
213            )
214        })?;
215        Ok(meters_per_pixel)
216    }
217}
218
219#[cfg(test)]
220mod tests {
221    use static_assertions::assert_impl_all;
222
223    use super::*;
224    use crate::{ErrorKind, MapOptions, RuntimeHandle};
225
226    assert_impl_all!(MapProjectionHandle: Send, Sync);
227
228    #[test]
229    // Spec coverage: BND-043 and BND-103.
230    fn projection_create_round_trip_close_and_stays_live_after_map_close() {
231        let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
232        let map = MapHandle::with_options(&runtime, &MapOptions::new(512, 512, 1.0)).unwrap();
233        let center = LatLng::new(37.7749, -122.4194);
234        let mut camera_options = CameraOptions::default();
235        camera_options.center = Some(center);
236        camera_options.zoom = Some(5.0);
237        map.jump_to(&camera_options).unwrap();
238
239        let projection = map.create_projection().unwrap();
240        map.close().unwrap();
241        runtime.close().unwrap();
242
243        std::thread::spawn(move || {
244            let point = projection.pixel_for_lat_lng(center).unwrap();
245            let round_tripped = projection.lat_lng_for_pixel(point).unwrap();
246            assert!((round_tripped.latitude - center.latitude).abs() < 1e-7);
247            assert!((round_tripped.longitude - center.longitude).abs() < 1e-7);
248            projection.close().unwrap();
249        })
250        .join()
251        .unwrap();
252    }
253
254    #[test]
255    // Rust regression: dropping a projection without explicit close must not
256    // attempt unsafe cleanup from an uncontrolled destructor path.
257    fn projection_drops_without_explicit_close() {
258        let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
259        let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap();
260
261        {
262            let _projection = map.create_projection().unwrap();
263        }
264
265        map.close().unwrap();
266        runtime.close().unwrap();
267    }
268
269    #[test]
270    // Spec coverage: BND-103.
271    fn projection_camera_and_visible_region_helpers_call_c_api() {
272        let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
273        let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap();
274        let projection = map.create_projection().unwrap();
275
276        let mut camera_options = CameraOptions::default();
277        camera_options.center = Some(LatLng::new(0.0, 0.0));
278        camera_options.zoom = Some(2.0);
279        projection.set_camera(&camera_options).unwrap();
280        let camera = projection.camera().unwrap();
281        assert_eq!(camera.center, Some(LatLng::new(0.0, 0.0)));
282        assert_eq!(camera.zoom, Some(2.0));
283
284        let padding = EdgeInsets::new(0.0, 0.0, 0.0, 0.0);
285        projection
286            .set_visible_coordinates(&[LatLng::new(0.0, 0.0), LatLng::new(1.0, 1.0)], padding)
287            .unwrap();
288        let error = projection
289            .set_visible_coordinates(&[], padding)
290            .unwrap_err();
291        assert_eq!(error.kind(), ErrorKind::InvalidArgument);
292        assert_eq!(error.raw_status(), None);
293        assert!(error.diagnostic().contains("at least one coordinate"));
294        projection
295            .set_visible_geometry(
296                br#"{"type":"LineString","coordinates":[[0.0,0.0],[1.0,1.0]]}"#,
297                padding,
298            )
299            .unwrap();
300
301        projection.close().unwrap();
302        map.close().unwrap();
303        runtime.close().unwrap();
304    }
305}