Skip to main content

waterkit_location/
lib.rs

1//! Cross-platform location access.
2//!
3//! Provides a unified async API for the device's geographic location.
4//! Callers must arrange for [`Permission::Location`] to be granted before
5//! calling [`Location::get`]; the recommended flow is
6//! `waterkit_permission::request(Permission::Location).await` then
7//! `Location::get().await`.
8//!
9//! # Example
10//!
11//! ```no_run
12//! use waterkit_location::Location;
13//! use waterkit_permission::{request, Permission};
14//!
15//! # async fn example() -> Result<(), waterkit_location::LocationError> {
16//! let _ = request(Permission::Location).await;
17//! let location = Location::get().await?;
18//! let _latitude = location.latitude().get();
19//! let _longitude = location.longitude().get();
20//! # Ok(())
21//! # }
22//! ```
23
24#![warn(missing_docs)]
25#![warn(missing_debug_implementations)]
26
27pub use jiff::Timestamp;
28pub use waterkit_core::{Latitude, Longitude, OutOfRange};
29
30mod sys;
31
32pub use waterkit_permission::{Permission, PermissionStatus};
33
34/// Android-specific JNI helpers that require an `Env` and `Context`/`Activity`.
35///
36/// These are intentionally separate from the async public API because Android
37/// permission/location flows require an app-owned JNI context.
38#[cfg(target_os = "android")]
39pub mod android {
40    pub use crate::sys::android::get_location_with_context;
41}
42
43/// A geographic location with coordinates and metadata.
44///
45/// All fields are private to allow future API evolution without breaking changes.
46/// Use the accessor methods to retrieve location data.
47#[derive(Debug, Clone, PartialEq)]
48pub struct Location {
49    latitude: Latitude,
50    longitude: Longitude,
51    altitude: Option<f64>,
52    horizontal_accuracy: Option<f64>,
53    vertical_accuracy: Option<f64>,
54    timestamp: Timestamp,
55}
56
57impl Location {
58    /// Creates a new `Location` with validated coordinates.
59    ///
60    /// # Arguments
61    ///
62    /// * `latitude` - Latitude in degrees (-90 to 90).
63    /// * `longitude` - Longitude in degrees (-180 to 180).
64    /// * `timestamp` - When this location was recorded
65    #[must_use]
66    pub const fn new(latitude: Latitude, longitude: Longitude, timestamp: Timestamp) -> Self {
67        Self {
68            latitude,
69            longitude,
70            altitude: None,
71            horizontal_accuracy: None,
72            vertical_accuracy: None,
73            timestamp,
74        }
75    }
76
77    /// Creates a new `Location` from raw coordinate degrees.
78    ///
79    /// # Errors
80    ///
81    /// Returns [`LocationError::InvalidCoordinate`] if either coordinate is
82    /// `NaN` or outside its valid range.
83    pub fn from_degrees(
84        latitude: f64,
85        longitude: f64,
86        timestamp: Timestamp,
87    ) -> Result<Self, LocationError> {
88        Ok(Self::new(
89            Latitude::new(latitude)?,
90            Longitude::new(longitude)?,
91            timestamp,
92        ))
93    }
94
95    /// Returns the current device location.
96    ///
97    /// **Precondition**: callers must ensure [`Permission::Location`] is
98    /// granted; this function does not trigger the runtime prompt.
99    /// Use `waterkit_permission::request(Permission::Location)` first.
100    ///
101    /// # Errors
102    ///
103    /// Returns [`LocationError::PermissionDenied`] when access is denied,
104    /// [`LocationError::ServiceDisabled`] when location services are off,
105    /// [`LocationError::Timeout`] when the request times out,
106    /// [`LocationError::InvalidCoordinate`] when the OS returns invalid
107    /// coordinates, or [`LocationError::Platform`] for other OS failures.
108    pub async fn get() -> Result<Self, LocationError> {
109        sys::get_location().await
110    }
111
112    /// Sets the altitude in meters above sea level.
113    ///
114    /// `with_*` prefix is preserved here because [`Location`] also has
115    /// getters with bare field names (`altitude()` returns the current
116    /// value); the workspace-wide naming rule is "bare method name =
117    /// setter unless a getter already takes that name."
118    #[must_use]
119    pub const fn with_altitude(mut self, altitude: f64) -> Self {
120        self.altitude = Some(altitude);
121        self
122    }
123
124    /// Sets the horizontal accuracy in meters.
125    #[must_use]
126    pub const fn with_horizontal_accuracy(mut self, accuracy: f64) -> Self {
127        self.horizontal_accuracy = Some(accuracy);
128        self
129    }
130
131    /// Sets the vertical accuracy in meters.
132    #[must_use]
133    pub const fn with_vertical_accuracy(mut self, accuracy: f64) -> Self {
134        self.vertical_accuracy = Some(accuracy);
135        self
136    }
137
138    /// Returns the latitude in degrees (-90 to 90).
139    #[must_use]
140    pub const fn latitude(&self) -> Latitude {
141        self.latitude
142    }
143
144    /// Returns the longitude in degrees (-180 to 180).
145    #[must_use]
146    pub const fn longitude(&self) -> Longitude {
147        self.longitude
148    }
149
150    /// Returns the altitude in meters above sea level, if available.
151    #[must_use]
152    pub const fn altitude(&self) -> Option<f64> {
153        self.altitude
154    }
155
156    /// Returns the horizontal accuracy in meters, if available.
157    ///
158    /// Lower values indicate more precise location data.
159    #[must_use]
160    pub const fn horizontal_accuracy(&self) -> Option<f64> {
161        self.horizontal_accuracy
162    }
163
164    /// Returns the vertical accuracy in meters, if available.
165    ///
166    /// Lower values indicate more precise altitude data.
167    #[must_use]
168    pub const fn vertical_accuracy(&self) -> Option<f64> {
169        self.vertical_accuracy
170    }
171
172    /// Returns the timestamp when this location was recorded.
173    #[must_use]
174    pub const fn timestamp(&self) -> Timestamp {
175        self.timestamp
176    }
177}
178
179/// Errors that can occur when accessing location.
180#[derive(Debug, Clone, thiserror::Error)]
181#[non_exhaustive]
182pub enum LocationError {
183    /// Location permission was not granted.
184    #[error("location permission denied")]
185    PermissionDenied,
186    /// Location services are disabled on the device.
187    #[error("location services disabled")]
188    ServiceDisabled,
189    /// Location request timed out.
190    #[error("location request timed out")]
191    Timeout,
192    /// Location is not available.
193    #[error("location not available")]
194    NotAvailable,
195    /// Location coordinates were outside their valid geographic ranges.
196    #[error("invalid location coordinate: {0}")]
197    InvalidCoordinate(#[from] OutOfRange),
198    /// Platform-level failure with a message.
199    #[error("platform error: {0}")]
200    Platform(String),
201}
202
203#[cfg(test)]
204mod tests {
205    use super::{Latitude, Location, LocationError, Longitude, Timestamp};
206
207    #[test]
208    fn new_stores_validated_coordinates() {
209        let location = Location::new(
210            Latitude::new(35.0).expect("valid latitude"),
211            Longitude::new(139.0).expect("valid longitude"),
212            Timestamp::from_second(0).expect("valid timestamp"),
213        );
214
215        assert!((location.latitude().get() - 35.0).abs() < f64::EPSILON);
216        assert!((location.longitude().get() - 139.0).abs() < f64::EPSILON);
217    }
218
219    #[test]
220    fn from_degrees_rejects_invalid_coordinates() {
221        let err = Location::from_degrees(
222            91.0,
223            139.0,
224            Timestamp::from_second(0).expect("valid timestamp"),
225        )
226        .expect_err("latitude should be rejected");
227
228        assert!(matches!(err, LocationError::InvalidCoordinate(_)));
229    }
230}