Skip to main content

spatial_narrative/core/
location.rs

1//! Geographic location representation.
2
3use crate::error::{Error, Result};
4use serde::{Deserialize, Serialize};
5
6/// A geographic location using WGS84 coordinates.
7///
8/// Locations are the fundamental spatial unit in spatial narratives.
9/// They support optional elevation and uncertainty for real-world data.
10///
11/// # Examples
12///
13/// ```
14/// use spatial_narrative::core::Location;
15///
16/// // Create a location (New York City)
17/// let nyc = Location::new(40.7128, -74.0060);
18/// assert!(nyc.is_valid());
19///
20/// // With elevation (Mount Everest)
21/// let peak = Location::with_elevation(27.9881, 86.9250, 8848.86);
22///
23/// // Using the builder for more options
24/// let approximate = Location::builder()
25///     .coordinates(40.7, -74.0)
26///     .uncertainty_meters(1000.0)
27///     .name("Approximate NYC")
28///     .build()
29///     .unwrap();
30/// ```
31///
32/// # Coordinate System
33///
34/// Locations use WGS84 (EPSG:4326):
35/// - Latitude: -90° to +90° (negative = South)
36/// - Longitude: -180° to +180° (negative = West)
37/// - Elevation: meters above sea level (optional)
38#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
39pub struct Location {
40    /// Latitude in decimal degrees (-90 to 90).
41    pub lat: f64,
42    /// Longitude in decimal degrees (-180 to 180).
43    pub lon: f64,
44    /// Elevation in meters above sea level.
45    #[serde(skip_serializing_if = "Option::is_none")]
46    pub elevation: Option<f64>,
47    /// Uncertainty radius in meters.
48    #[serde(skip_serializing_if = "Option::is_none")]
49    pub uncertainty_meters: Option<f64>,
50    /// Human-readable place name.
51    #[serde(skip_serializing_if = "Option::is_none")]
52    pub name: Option<String>,
53}
54
55impl Location {
56    /// Creates a new location with the given latitude and longitude.
57    ///
58    /// # Arguments
59    ///
60    /// * `lat` - Latitude in decimal degrees (-90 to 90)
61    /// * `lon` - Longitude in decimal degrees (-180 to 180)
62    ///
63    /// # Examples
64    ///
65    /// ```
66    /// use spatial_narrative::core::Location;
67    ///
68    /// let loc = Location::new(51.5074, -0.1278); // London
69    /// ```
70    pub fn new(lat: f64, lon: f64) -> Self {
71        Self {
72            lat,
73            lon,
74            elevation: None,
75            uncertainty_meters: None,
76            name: None,
77        }
78    }
79
80    /// Creates a new location with elevation.
81    ///
82    /// # Arguments
83    ///
84    /// * `lat` - Latitude in decimal degrees
85    /// * `lon` - Longitude in decimal degrees
86    /// * `elevation` - Elevation in meters above sea level
87    pub fn with_elevation(lat: f64, lon: f64, elevation: f64) -> Self {
88        Self {
89            lat,
90            lon,
91            elevation: Some(elevation),
92            uncertainty_meters: None,
93            name: None,
94        }
95    }
96
97    /// Creates a new builder for constructing a Location.
98    pub fn builder() -> LocationBuilder {
99        LocationBuilder::new()
100    }
101
102    /// Checks if the coordinates are valid WGS84 values.
103    ///
104    /// Returns `true` if latitude is between -90 and 90,
105    /// and longitude is between -180 and 180.
106    pub fn is_valid(&self) -> bool {
107        self.lat >= -90.0 && self.lat <= 90.0 && self.lon >= -180.0 && self.lon <= 180.0
108    }
109
110    /// Validates the location, returning an error if invalid.
111    pub fn validate(&self) -> Result<()> {
112        if self.lat < -90.0 || self.lat > 90.0 {
113            return Err(Error::InvalidLatitude(self.lat));
114        }
115        if self.lon < -180.0 || self.lon > 180.0 {
116            return Err(Error::InvalidLongitude(self.lon));
117        }
118        Ok(())
119    }
120
121    /// Returns the coordinates as a tuple (lat, lon).
122    pub fn as_tuple(&self) -> (f64, f64) {
123        (self.lat, self.lon)
124    }
125
126    /// Returns the coordinates as a geo-types Point.
127    pub fn to_geo_point(&self) -> geo_types::Point<f64> {
128        geo_types::Point::new(self.lon, self.lat)
129    }
130
131    /// Creates a Location from a geo-types Point.
132    pub fn from_geo_point(point: geo_types::Point<f64>) -> Self {
133        Self::new(point.y(), point.x())
134    }
135}
136
137impl Default for Location {
138    fn default() -> Self {
139        Self::new(0.0, 0.0)
140    }
141}
142
143impl From<(f64, f64)> for Location {
144    fn from((lat, lon): (f64, f64)) -> Self {
145        Self::new(lat, lon)
146    }
147}
148
149impl From<geo_types::Point<f64>> for Location {
150    fn from(point: geo_types::Point<f64>) -> Self {
151        Self::from_geo_point(point)
152    }
153}
154
155/// Builder for constructing [`Location`] instances.
156#[derive(Debug, Default)]
157pub struct LocationBuilder {
158    lat: Option<f64>,
159    lon: Option<f64>,
160    elevation: Option<f64>,
161    uncertainty_meters: Option<f64>,
162    name: Option<String>,
163}
164
165impl LocationBuilder {
166    /// Creates a new LocationBuilder.
167    pub fn new() -> Self {
168        Self::default()
169    }
170
171    /// Sets the latitude.
172    pub fn lat(mut self, lat: f64) -> Self {
173        self.lat = Some(lat);
174        self
175    }
176
177    /// Sets the longitude.
178    pub fn lon(mut self, lon: f64) -> Self {
179        self.lon = Some(lon);
180        self
181    }
182
183    /// Sets both latitude and longitude.
184    pub fn coordinates(mut self, lat: f64, lon: f64) -> Self {
185        self.lat = Some(lat);
186        self.lon = Some(lon);
187        self
188    }
189
190    /// Sets the elevation in meters.
191    pub fn elevation(mut self, elevation: f64) -> Self {
192        self.elevation = Some(elevation);
193        self
194    }
195
196    /// Sets the uncertainty radius in meters.
197    pub fn uncertainty_meters(mut self, uncertainty: f64) -> Self {
198        self.uncertainty_meters = Some(uncertainty);
199        self
200    }
201
202    /// Sets the place name.
203    pub fn name(mut self, name: impl Into<String>) -> Self {
204        self.name = Some(name.into());
205        self
206    }
207
208    /// Builds the Location, returning an error if required fields are missing.
209    pub fn build(self) -> Result<Location> {
210        let lat = self.lat.ok_or(Error::MissingField("lat"))?;
211        let lon = self.lon.ok_or(Error::MissingField("lon"))?;
212
213        let location = Location {
214            lat,
215            lon,
216            elevation: self.elevation,
217            uncertainty_meters: self.uncertainty_meters,
218            name: self.name,
219        };
220
221        location.validate()?;
222        Ok(location)
223    }
224}
225
226#[cfg(test)]
227mod tests {
228    use super::*;
229
230    #[test]
231    fn test_location_new() {
232        let loc = Location::new(40.7128, -74.0060);
233        assert_eq!(loc.lat, 40.7128);
234        assert_eq!(loc.lon, -74.0060);
235        assert!(loc.elevation.is_none());
236        assert!(loc.is_valid());
237    }
238
239    #[test]
240    fn test_location_with_elevation() {
241        let loc = Location::with_elevation(27.9881, 86.9250, 8848.86);
242        assert_eq!(loc.elevation, Some(8848.86));
243    }
244
245    #[test]
246    fn test_location_validation() {
247        let valid = Location::new(45.0, 90.0);
248        assert!(valid.is_valid());
249        assert!(valid.validate().is_ok());
250
251        let invalid_lat = Location::new(91.0, 0.0);
252        assert!(!invalid_lat.is_valid());
253        assert!(invalid_lat.validate().is_err());
254
255        let invalid_lon = Location::new(0.0, 181.0);
256        assert!(!invalid_lon.is_valid());
257        assert!(invalid_lon.validate().is_err());
258    }
259
260    #[test]
261    fn test_location_builder() {
262        let loc = Location::builder()
263            .coordinates(51.5074, -0.1278)
264            .elevation(11.0)
265            .uncertainty_meters(10.0)
266            .name("London")
267            .build()
268            .unwrap();
269
270        assert_eq!(loc.lat, 51.5074);
271        assert_eq!(loc.lon, -0.1278);
272        assert_eq!(loc.elevation, Some(11.0));
273        assert_eq!(loc.uncertainty_meters, Some(10.0));
274        assert_eq!(loc.name, Some("London".to_string()));
275    }
276
277    #[test]
278    fn test_location_builder_missing_fields() {
279        let result = Location::builder().lat(40.0).build();
280        assert!(result.is_err());
281    }
282
283    #[test]
284    fn test_location_from_tuple() {
285        let loc: Location = (40.7128, -74.0060).into();
286        assert_eq!(loc.lat, 40.7128);
287        assert_eq!(loc.lon, -74.0060);
288    }
289
290    #[test]
291    fn test_location_to_geo_point() {
292        let loc = Location::new(40.7128, -74.0060);
293        let point = loc.to_geo_point();
294        assert_eq!(point.x(), -74.0060);
295        assert_eq!(point.y(), 40.7128);
296    }
297
298    #[test]
299    fn test_location_serialization() {
300        let loc = Location::new(40.7128, -74.0060);
301        let json = serde_json::to_string(&loc).unwrap();
302        let parsed: Location = serde_json::from_str(&json).unwrap();
303        assert_eq!(loc, parsed);
304    }
305}