Skip to main content

maplibre_native/
projection.rs

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