1use serde::{Deserialize, Serialize};
6use validator::Validate;
7
8use super::common::{CovarianceMatrix, Position2D, Position3D};
9
10#[derive(Debug, Clone, Default, Serialize, Deserialize, Validate)]
12pub struct LocationDomain {
13 #[serde(skip_serializing_if = "Option::is_none")]
16 #[validate(range(min = -90.0, max = 90.0))]
17 pub latitude: Option<f64>,
18
19 #[serde(skip_serializing_if = "Option::is_none")]
21 #[validate(range(min = -180.0, max = 180.0))]
22 pub longitude: Option<f64>,
23
24 #[serde(skip_serializing_if = "Option::is_none")]
26 pub altitude_m: Option<f64>,
27
28 #[serde(skip_serializing_if = "Option::is_none")]
30 #[validate(range(min = 0.0, max = 360.0))]
31 pub heading_deg: Option<f64>,
32
33 #[serde(skip_serializing_if = "Option::is_none")]
36 pub local: Option<LocalCoordinates>,
37
38 #[serde(skip_serializing_if = "Option::is_none")]
41 pub grid: Option<GridCoordinates>,
42
43 #[serde(skip_serializing_if = "Option::is_none")]
46 pub semantic: Option<SemanticLocation>,
47
48 #[serde(skip_serializing_if = "Option::is_none")]
51 #[validate(range(min = 0.0))]
52 pub horizontal_accuracy_m: Option<f64>,
53
54 #[serde(skip_serializing_if = "Option::is_none")]
56 #[validate(range(min = 0.0))]
57 pub vertical_accuracy_m: Option<f64>,
58
59 #[serde(skip_serializing_if = "Option::is_none")]
61 pub covariance: Option<CovarianceMatrix>,
62
63 #[serde(skip_serializing_if = "Option::is_none")]
66 pub frame_id: Option<String>,
67
68 #[serde(skip_serializing_if = "Option::is_none")]
70 pub map_id: Option<String>,
71
72 #[serde(skip_serializing_if = "Option::is_none")]
74 pub floor: Option<i32>,
75}
76
77#[derive(Debug, Clone, Default, Serialize, Deserialize, Validate)]
79pub struct LocalCoordinates {
80 #[serde(skip_serializing_if = "Option::is_none")]
82 pub x_m: Option<f64>,
83
84 #[serde(skip_serializing_if = "Option::is_none")]
86 pub y_m: Option<f64>,
87
88 #[serde(skip_serializing_if = "Option::is_none")]
90 pub z_m: Option<f64>,
91
92 #[serde(skip_serializing_if = "Option::is_none")]
94 #[validate(range(min = -180.0, max = 180.0))]
95 pub yaw_deg: Option<f64>,
96
97 #[serde(skip_serializing_if = "Option::is_none")]
99 #[validate(range(min = -180.0, max = 180.0))]
100 pub roll_deg: Option<f64>,
101
102 #[serde(skip_serializing_if = "Option::is_none")]
104 #[validate(range(min = -90.0, max = 90.0))]
105 pub pitch_deg: Option<f64>,
106}
107
108impl LocalCoordinates {
109 pub fn new(x_m: f64, y_m: f64, yaw_deg: f64) -> Self {
111 Self {
112 x_m: Some(x_m),
113 y_m: Some(y_m),
114 z_m: None,
115 yaw_deg: Some(yaw_deg),
116 roll_deg: None,
117 pitch_deg: None,
118 }
119 }
120
121 pub fn new_3d(x_m: f64, y_m: f64, z_m: f64, yaw_deg: f64) -> Self {
123 Self {
124 x_m: Some(x_m),
125 y_m: Some(y_m),
126 z_m: Some(z_m),
127 yaw_deg: Some(yaw_deg),
128 roll_deg: None,
129 pitch_deg: None,
130 }
131 }
132
133 pub fn to_position_2d(&self) -> Position2D {
135 Position2D {
136 x_m: self.x_m,
137 y_m: self.y_m,
138 }
139 }
140
141 pub fn to_position_3d(&self) -> Position3D {
143 Position3D {
144 x_m: self.x_m,
145 y_m: self.y_m,
146 z_m: self.z_m,
147 }
148 }
149}
150
151#[derive(Debug, Clone, Default, Serialize, Deserialize)]
153pub struct GridCoordinates {
154 #[serde(skip_serializing_if = "Option::is_none")]
156 pub row: Option<i32>,
157
158 #[serde(skip_serializing_if = "Option::is_none")]
160 pub column: Option<i32>,
161
162 #[serde(skip_serializing_if = "Option::is_none")]
164 pub aisle: Option<String>,
165
166 #[serde(skip_serializing_if = "Option::is_none")]
168 pub bay: Option<String>,
169
170 #[serde(skip_serializing_if = "Option::is_none")]
172 pub level: Option<i32>,
173
174 #[serde(skip_serializing_if = "Option::is_none")]
176 pub cell: Option<String>,
177}
178
179#[derive(Debug, Clone, Default, Serialize, Deserialize)]
181pub struct SemanticLocation {
182 #[serde(skip_serializing_if = "Option::is_none")]
184 pub zone: Option<String>,
185
186 #[serde(skip_serializing_if = "Option::is_none")]
188 pub zone_type: Option<String>,
189
190 #[serde(skip_serializing_if = "Option::is_none")]
192 pub area: Option<String>,
193
194 #[serde(skip_serializing_if = "Option::is_none")]
196 pub building: Option<String>,
197
198 #[serde(skip_serializing_if = "Option::is_none")]
200 pub room: Option<String>,
201
202 #[serde(skip_serializing_if = "Option::is_none")]
204 pub landmark: Option<String>,
205
206 #[serde(skip_serializing_if = "Option::is_none")]
208 pub waypoint: Option<String>,
209
210 #[serde(skip_serializing_if = "Option::is_none")]
212 pub node_id: Option<String>,
213
214 #[serde(skip_serializing_if = "Option::is_none")]
216 pub edge_id: Option<String>,
217}
218
219impl LocationDomain {
220 pub fn from_global(latitude: f64, longitude: f64) -> Self {
222 Self {
223 latitude: Some(latitude),
224 longitude: Some(longitude),
225 ..Default::default()
226 }
227 }
228
229 pub fn from_local(x_m: f64, y_m: f64, yaw_deg: f64) -> Self {
231 Self {
232 local: Some(LocalCoordinates::new(x_m, y_m, yaw_deg)),
233 ..Default::default()
234 }
235 }
236
237 pub fn with_heading(mut self, heading_deg: f64) -> Self {
239 self.heading_deg = Some(heading_deg);
240 self
241 }
242
243 pub fn with_local(mut self, local: LocalCoordinates) -> Self {
245 self.local = Some(local);
246 self
247 }
248
249 pub fn with_semantic(mut self, zone: impl Into<String>) -> Self {
251 self.semantic = Some(SemanticLocation {
252 zone: Some(zone.into()),
253 ..Default::default()
254 });
255 self
256 }
257
258 pub fn with_accuracy(mut self, horizontal_m: f64) -> Self {
260 self.horizontal_accuracy_m = Some(horizontal_m);
261 self
262 }
263
264 pub fn with_frame(mut self, frame_id: impl Into<String>) -> Self {
266 self.frame_id = Some(frame_id.into());
267 self
268 }
269}
270
271#[cfg(test)]
272mod tests {
273 use super::*;
274
275 #[test]
276 fn test_location_from_global() {
277 let loc = LocationDomain::from_global(41.8781, -87.6298).with_heading(45.0);
278
279 assert_eq!(loc.latitude, Some(41.8781));
280 assert_eq!(loc.longitude, Some(-87.6298));
281 assert_eq!(loc.heading_deg, Some(45.0));
282 }
283
284 #[test]
285 fn test_location_from_local() {
286 let loc = LocationDomain::from_local(10.5, 20.3, 90.0);
287
288 let local = loc.local.unwrap();
289 assert_eq!(local.x_m, Some(10.5));
290 assert_eq!(local.y_m, Some(20.3));
291 assert_eq!(local.yaw_deg, Some(90.0));
292 }
293
294 #[test]
295 fn test_location_serialization() {
296 let loc = LocationDomain::from_global(41.8781, -87.6298);
297 let json = serde_json::to_string(&loc).unwrap();
298
299 assert!(json.contains("41.8781"));
300 assert!(json.contains("-87.6298"));
301 }
302
303 #[test]
308 fn test_location_domain_default() {
309 let loc = LocationDomain::default();
310 assert!(loc.latitude.is_none());
311 assert!(loc.longitude.is_none());
312 assert!(loc.altitude_m.is_none());
313 assert!(loc.heading_deg.is_none());
314 assert!(loc.local.is_none());
315 assert!(loc.grid.is_none());
316 assert!(loc.semantic.is_none());
317 }
318
319 #[test]
320 fn test_location_domain_with_local() {
321 let local = LocalCoordinates::new(5.0, 10.0, 45.0);
322 let loc = LocationDomain::default().with_local(local);
323
324 assert!(loc.local.is_some());
325 assert_eq!(loc.local.as_ref().unwrap().x_m, Some(5.0));
326 }
327
328 #[test]
329 fn test_location_domain_with_semantic() {
330 let loc = LocationDomain::default().with_semantic("warehouse_zone_a");
331
332 assert!(loc.semantic.is_some());
333 assert_eq!(
334 loc.semantic.as_ref().unwrap().zone,
335 Some("warehouse_zone_a".to_string())
336 );
337 }
338
339 #[test]
340 fn test_location_domain_with_accuracy() {
341 let loc = LocationDomain::default().with_accuracy(0.5);
342 assert_eq!(loc.horizontal_accuracy_m, Some(0.5));
343 }
344
345 #[test]
346 fn test_location_domain_with_frame() {
347 let loc = LocationDomain::default().with_frame("map");
348 assert_eq!(loc.frame_id, Some("map".to_string()));
349 }
350
351 #[test]
352 fn test_location_domain_chained_builders() {
353 let loc = LocationDomain::from_global(40.0, -74.0)
354 .with_heading(180.0)
355 .with_local(LocalCoordinates::new(0.0, 0.0, 0.0))
356 .with_semantic("parking")
357 .with_accuracy(1.0)
358 .with_frame("odom");
359
360 assert!(loc.latitude.is_some());
361 assert!(loc.heading_deg.is_some());
362 assert!(loc.local.is_some());
363 assert!(loc.semantic.is_some());
364 assert!(loc.horizontal_accuracy_m.is_some());
365 assert!(loc.frame_id.is_some());
366 }
367
368 #[test]
373 fn test_local_coordinates_new() {
374 let local = LocalCoordinates::new(1.0, 2.0, 90.0);
375 assert_eq!(local.x_m, Some(1.0));
376 assert_eq!(local.y_m, Some(2.0));
377 assert!(local.z_m.is_none());
378 assert_eq!(local.yaw_deg, Some(90.0));
379 }
380
381 #[test]
382 fn test_local_coordinates_new_3d() {
383 let local = LocalCoordinates::new_3d(1.0, 2.0, 3.0, 45.0);
384 assert_eq!(local.x_m, Some(1.0));
385 assert_eq!(local.y_m, Some(2.0));
386 assert_eq!(local.z_m, Some(3.0));
387 assert_eq!(local.yaw_deg, Some(45.0));
388 }
389
390 #[test]
391 fn test_local_coordinates_to_position_2d() {
392 let local = LocalCoordinates::new(10.0, 20.0, 0.0);
393 let pos2d = local.to_position_2d();
394
395 assert_eq!(pos2d.x_m, Some(10.0));
396 assert_eq!(pos2d.y_m, Some(20.0));
397 }
398
399 #[test]
400 fn test_local_coordinates_to_position_3d() {
401 let local = LocalCoordinates::new_3d(1.0, 2.0, 3.0, 0.0);
402 let pos3d = local.to_position_3d();
403
404 assert_eq!(pos3d.x_m, Some(1.0));
405 assert_eq!(pos3d.y_m, Some(2.0));
406 assert_eq!(pos3d.z_m, Some(3.0));
407 }
408
409 #[test]
410 fn test_local_coordinates_default() {
411 let local = LocalCoordinates::default();
412 assert!(local.x_m.is_none());
413 assert!(local.y_m.is_none());
414 assert!(local.z_m.is_none());
415 assert!(local.yaw_deg.is_none());
416 assert!(local.roll_deg.is_none());
417 assert!(local.pitch_deg.is_none());
418 }
419
420 #[test]
425 fn test_grid_coordinates_default() {
426 let grid = GridCoordinates::default();
427 assert!(grid.row.is_none());
428 assert!(grid.column.is_none());
429 assert!(grid.aisle.is_none());
430 assert!(grid.bay.is_none());
431 assert!(grid.level.is_none());
432 assert!(grid.cell.is_none());
433 }
434
435 #[test]
436 fn test_grid_coordinates_full() {
437 let grid = GridCoordinates {
438 row: Some(5),
439 column: Some(10),
440 aisle: Some("A".to_string()),
441 bay: Some("B3".to_string()),
442 level: Some(2),
443 cell: Some("A-B3-5-10".to_string()),
444 };
445
446 assert_eq!(grid.row, Some(5));
447 assert_eq!(grid.aisle, Some("A".to_string()));
448 }
449
450 #[test]
455 fn test_semantic_location_default() {
456 let sem = SemanticLocation::default();
457 assert!(sem.zone.is_none());
458 assert!(sem.zone_type.is_none());
459 assert!(sem.area.is_none());
460 assert!(sem.building.is_none());
461 assert!(sem.room.is_none());
462 }
463
464 #[test]
465 fn test_semantic_location_full() {
466 let sem = SemanticLocation {
467 zone: Some("picking_zone".to_string()),
468 zone_type: Some("work_area".to_string()),
469 area: Some("east_wing".to_string()),
470 building: Some("Warehouse A".to_string()),
471 room: Some("Storage 101".to_string()),
472 landmark: Some("Charger Station 1".to_string()),
473 waypoint: Some("wp_001".to_string()),
474 node_id: Some("node_42".to_string()),
475 edge_id: Some("edge_42_43".to_string()),
476 };
477
478 assert_eq!(sem.zone, Some("picking_zone".to_string()));
479 assert_eq!(sem.waypoint, Some("wp_001".to_string()));
480 }
481
482 #[test]
487 fn test_location_domain_serialization_roundtrip() {
488 let loc = LocationDomain {
489 latitude: Some(41.8781),
490 longitude: Some(-87.6298),
491 altitude_m: Some(200.0),
492 heading_deg: Some(90.0),
493 local: Some(LocalCoordinates::new_3d(10.0, 20.0, 0.0, 90.0)),
494 horizontal_accuracy_m: Some(0.5),
495 frame_id: Some("map".to_string()),
496 floor: Some(1),
497 ..Default::default()
498 };
499
500 let json = serde_json::to_string(&loc).unwrap();
501 let deserialized: LocationDomain = serde_json::from_str(&json).unwrap();
502
503 assert_eq!(deserialized.latitude, Some(41.8781));
504 assert_eq!(deserialized.floor, Some(1));
505 assert!(deserialized.local.is_some());
506 }
507}