spatial_narrative/core/
location.rs1use crate::error::{Error, Result};
4use serde::{Deserialize, Serialize};
5
6#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
39pub struct Location {
40 pub lat: f64,
42 pub lon: f64,
44 #[serde(skip_serializing_if = "Option::is_none")]
46 pub elevation: Option<f64>,
47 #[serde(skip_serializing_if = "Option::is_none")]
49 pub uncertainty_meters: Option<f64>,
50 #[serde(skip_serializing_if = "Option::is_none")]
52 pub name: Option<String>,
53}
54
55impl Location {
56 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 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 pub fn builder() -> LocationBuilder {
99 LocationBuilder::new()
100 }
101
102 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 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 pub fn as_tuple(&self) -> (f64, f64) {
123 (self.lat, self.lon)
124 }
125
126 pub fn to_geo_point(&self) -> geo_types::Point<f64> {
128 geo_types::Point::new(self.lon, self.lat)
129 }
130
131 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#[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 pub fn new() -> Self {
168 Self::default()
169 }
170
171 pub fn lat(mut self, lat: f64) -> Self {
173 self.lat = Some(lat);
174 self
175 }
176
177 pub fn lon(mut self, lon: f64) -> Self {
179 self.lon = Some(lon);
180 self
181 }
182
183 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 pub fn elevation(mut self, elevation: f64) -> Self {
192 self.elevation = Some(elevation);
193 self
194 }
195
196 pub fn uncertainty_meters(mut self, uncertainty: f64) -> Self {
198 self.uncertainty_meters = Some(uncertainty);
199 self
200 }
201
202 pub fn name(mut self, name: impl Into<String>) -> Self {
204 self.name = Some(name.into());
205 self
206 }
207
208 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}