Skip to content

Commit 9a4753b

Browse files
authored
Add a helper type for finding multi-step reference frame transformations (#111)
We've discussed adding this, and now I've had a real need to do this. This is the MVP implementation for helping find the "shortest path" series of transformations. This is accomplished by building up a graph with the reference frames as vertices and the transformations as edges, then performing a breadth-first search through this graph for the first available path. # Long Term Improvements I've implemented this functionality as an additional function to avoid breaking API changes. In a future revision I would like to roll up this functionality into the `get_transformation()` function and make it transparent, but doing that in the way I am thinking would require adding a new type representing a compound transformation and modifying the `Transformation` struct to be able to use it.
1 parent f699414 commit 9a4753b

File tree

1 file changed

+111
-2
lines changed
  • swiftnav/src/reference_frame

1 file changed

+111
-2
lines changed

swiftnav/src/reference_frame/mod.rs

Lines changed: 111 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,13 +75,18 @@
7575
//!
7676
7777
use crate::coords::{Coordinate, ECEF};
78-
use std::fmt;
78+
use std::{
79+
collections::{HashMap, HashSet, VecDeque},
80+
fmt,
81+
};
7982
use strum::{Display, EnumIter, EnumString};
8083

8184
mod params;
8285

8386
/// 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+
)]
8590
#[strum(serialize_all = "UPPERCASE")]
8691
pub enum ReferenceFrame {
8792
ITRF88,
@@ -288,11 +293,81 @@ pub fn get_transformation(
288293
.ok_or(TransformationNotFound(from, to))
289294
}
290295

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(&current_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+
291364
#[cfg(test)]
292365
mod tests {
293366
use super::*;
294367
use float_eq::assert_float_eq;
368+
use params::TRANSFORMATIONS;
295369
use std::str::FromStr;
370+
use strum::IntoEnumIterator;
296371

297372
#[test]
298373
fn reference_frame_strings() {
@@ -678,4 +753,38 @@ mod tests {
678753
assert_float_eq!(params.rz_dot, 0.7, abs_all <= 1e-4);
679754
assert_float_eq!(params.epoch, 2010.0, abs_all <= 1e-4);
680755
}
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+
}
681790
}

0 commit comments

Comments
 (0)