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}