|
75 | 75 | //!
|
76 | 76 |
|
77 | 77 | use crate::coords::{Coordinate, ECEF};
|
78 |
| -use std::fmt; |
| 78 | +use std::{ |
| 79 | + collections::{HashMap, HashSet, VecDeque}, |
| 80 | + fmt, |
| 81 | +}; |
79 | 82 | use strum::{Display, EnumIter, EnumString};
|
80 | 83 |
|
81 | 84 | mod params;
|
82 | 85 |
|
83 | 86 | /// Reference Frames
|
84 |
| -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, EnumString, Display, EnumIter)] |
| 87 | +#[derive( |
| 88 | + Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, EnumString, Display, EnumIter, Hash, |
| 89 | +)] |
85 | 90 | #[strum(serialize_all = "UPPERCASE")]
|
86 | 91 | pub enum ReferenceFrame {
|
87 | 92 | ITRF88,
|
@@ -288,11 +293,81 @@ pub fn get_transformation(
|
288 | 293 | .ok_or(TransformationNotFound(from, to))
|
289 | 294 | }
|
290 | 295 |
|
| 296 | +/// A helper type for finding transformations between reference frames that require multiple steps |
| 297 | +/// |
| 298 | +/// This object can be used to determine which calls to [`get_transformation`](crate::reference_frame::get_transformation) |
| 299 | +/// are needed when a single transformation does not exist between two reference frames. |
| 300 | +pub struct TransformationGraph { |
| 301 | + graph: HashMap<ReferenceFrame, HashSet<ReferenceFrame>>, |
| 302 | +} |
| 303 | + |
| 304 | +impl TransformationGraph { |
| 305 | + /// Create a new transformation graph, fully populated with the known transformations |
| 306 | + pub fn new() -> Self { |
| 307 | + let mut graph = HashMap::new(); |
| 308 | + for transformation in params::TRANSFORMATIONS.iter() { |
| 309 | + graph |
| 310 | + .entry(transformation.from) |
| 311 | + .or_insert_with(HashSet::new) |
| 312 | + .insert(transformation.to); |
| 313 | + graph |
| 314 | + .entry(transformation.to) |
| 315 | + .or_insert_with(HashSet::new) |
| 316 | + .insert(transformation.from); |
| 317 | + } |
| 318 | + TransformationGraph { graph } |
| 319 | + } |
| 320 | + |
| 321 | + /// Get the shortest path between two reference frames, if one exists |
| 322 | + /// |
| 323 | + /// This function will also search for reverse paths if no direct path is found. |
| 324 | + /// The search is performed breadth-first. |
| 325 | + pub fn get_shortest_path( |
| 326 | + &self, |
| 327 | + from: ReferenceFrame, |
| 328 | + to: ReferenceFrame, |
| 329 | + ) -> Option<Vec<ReferenceFrame>> { |
| 330 | + if from == to { |
| 331 | + return None; |
| 332 | + } |
| 333 | + |
| 334 | + let mut visited: HashSet<ReferenceFrame> = HashSet::new(); |
| 335 | + let mut queue: VecDeque<(ReferenceFrame, Vec<ReferenceFrame>)> = VecDeque::new(); |
| 336 | + queue.push_back((from, vec![from])); |
| 337 | + |
| 338 | + while let Some((current_frame, path)) = queue.pop_front() { |
| 339 | + if current_frame == to { |
| 340 | + return Some(path); |
| 341 | + } |
| 342 | + |
| 343 | + if let Some(neighbors) = self.graph.get(¤t_frame) { |
| 344 | + for neighbor in neighbors { |
| 345 | + if !visited.contains(neighbor) { |
| 346 | + visited.insert(*neighbor); |
| 347 | + let mut new_path = path.clone(); |
| 348 | + new_path.push(*neighbor); |
| 349 | + queue.push_back((*neighbor, new_path)); |
| 350 | + } |
| 351 | + } |
| 352 | + } |
| 353 | + } |
| 354 | + None |
| 355 | + } |
| 356 | +} |
| 357 | + |
| 358 | +impl Default for TransformationGraph { |
| 359 | + fn default() -> Self { |
| 360 | + TransformationGraph::new() |
| 361 | + } |
| 362 | +} |
| 363 | + |
291 | 364 | #[cfg(test)]
|
292 | 365 | mod tests {
|
293 | 366 | use super::*;
|
294 | 367 | use float_eq::assert_float_eq;
|
| 368 | + use params::TRANSFORMATIONS; |
295 | 369 | use std::str::FromStr;
|
| 370 | + use strum::IntoEnumIterator; |
296 | 371 |
|
297 | 372 | #[test]
|
298 | 373 | fn reference_frame_strings() {
|
@@ -678,4 +753,38 @@ mod tests {
|
678 | 753 | assert_float_eq!(params.rz_dot, 0.7, abs_all <= 1e-4);
|
679 | 754 | assert_float_eq!(params.epoch, 2010.0, abs_all <= 1e-4);
|
680 | 755 | }
|
| 756 | + |
| 757 | + #[test] |
| 758 | + fn itrf2020_to_etrf2000_shortest_path() { |
| 759 | + let from = ReferenceFrame::ITRF2020; |
| 760 | + let to = ReferenceFrame::ETRF2000; |
| 761 | + |
| 762 | + // Make sure there isn't a direct path |
| 763 | + assert!(!TRANSFORMATIONS.iter().any(|t| t.from == from && t.to == to)); |
| 764 | + |
| 765 | + let graph = TransformationGraph::new(); |
| 766 | + let path = graph.get_shortest_path(from, to); |
| 767 | + assert!(path.is_some()); |
| 768 | + // Make sure that the path is correct. N.B. this may change if more transformations |
| 769 | + // are added in the future |
| 770 | + let path = path.unwrap(); |
| 771 | + assert_eq!(path.len(), 3); |
| 772 | + assert_eq!(path[0], from); |
| 773 | + assert_eq!(path[1], ReferenceFrame::ITRF2000); |
| 774 | + assert_eq!(path[2], to); |
| 775 | + } |
| 776 | + |
| 777 | + #[test] |
| 778 | + fn fully_traversable_graph() { |
| 779 | + let graph = TransformationGraph::new(); |
| 780 | + for from in ReferenceFrame::iter() { |
| 781 | + for to in ReferenceFrame::iter() { |
| 782 | + if from == to { |
| 783 | + continue; |
| 784 | + } |
| 785 | + let path = graph.get_shortest_path(from, to); |
| 786 | + assert!(path.is_some(), "No path from {} to {}", from, to); |
| 787 | + } |
| 788 | + } |
| 789 | + } |
681 | 790 | }
|
0 commit comments