spatialrust_mapping/
trajectory.rs1use spatialrust_math::{Isometry3, Pose3};
4use spatialrust_sync::StampedTime;
5
6use crate::{MappingError, MappingResult};
7
8#[derive(Clone, Debug, PartialEq)]
10pub struct StampedPose {
11 pub stamp: StampedTime,
13 pub pose: Pose3<f32>,
15}
16
17impl StampedPose {
18 #[must_use]
20 pub fn new(stamp: StampedTime, pose: Pose3<f32>) -> Self {
21 Self { stamp, pose }
22 }
23}
24
25#[derive(Clone, Debug, Default, PartialEq)]
27pub struct Trajectory {
28 samples: Vec<StampedPose>,
29}
30
31impl Trajectory {
32 #[must_use]
34 pub fn new() -> Self {
35 Self::default()
36 }
37
38 pub fn push(&mut self, sample: StampedPose) -> MappingResult<()> {
40 if let Some(last) = self.samples.last() {
41 if sample.stamp.as_nanos() < last.stamp.as_nanos() {
42 return Err(MappingError::InvalidConfiguration(
43 "trajectory timestamps must be non-decreasing".into(),
44 ));
45 }
46 }
47 self.samples.push(sample);
48 Ok(())
49 }
50
51 #[must_use]
53 pub fn samples(&self) -> &[StampedPose] {
54 &self.samples
55 }
56
57 #[must_use]
59 pub fn last(&self) -> Option<&StampedPose> {
60 self.samples.last()
61 }
62
63 pub fn interpolate(&self, nanos: u64) -> MappingResult<Pose3<f32>> {
65 if self.samples.is_empty() {
66 return Err(MappingError::Missing("trajectory samples".into()));
67 }
68 if nanos <= self.samples[0].stamp.as_nanos() {
69 return Ok(self.samples[0].pose);
70 }
71 if let Some(last) = self.samples.last() {
72 if nanos >= last.stamp.as_nanos() {
73 return Ok(last.pose);
74 }
75 }
76 for window in self.samples.windows(2) {
77 let a = &window[0];
78 let b = &window[1];
79 if nanos >= a.stamp.as_nanos() && nanos <= b.stamp.as_nanos() {
80 let span = b.stamp.as_nanos() - a.stamp.as_nanos();
81 if span == 0 {
82 return Ok(a.pose);
83 }
84 let t = (nanos - a.stamp.as_nanos()) as f32 / span as f32;
85 let ta = a.pose.isometry.translation();
86 let tb = b.pose.isometry.translation();
87 let translation = spatialrust_math::Vec3::new(
88 ta.x + (tb.x - ta.x) * t,
89 ta.y + (tb.y - ta.y) * t,
90 ta.z + (tb.z - ta.z) * t,
91 );
92 return Ok(Pose3::new(Isometry3::new(a.pose.isometry.rotation(), translation)));
93 }
94 }
95 Err(MappingError::Graph("interpolation failed".into()))
96 }
97}
98
99#[cfg(test)]
100mod tests {
101 use super::{StampedPose, Trajectory};
102 use spatialrust_core::Timestamp;
103 use spatialrust_math::{Isometry3, Pose3, Quat, Vec3};
104 use spatialrust_sync::{ClockDomain, StampedTime};
105
106 #[test]
107 fn interpolates_translation() {
108 let mut traj = Trajectory::new();
109 traj.push(StampedPose::new(
110 StampedTime::exact("host", ClockDomain::HostSteady, Timestamp::from_nanos(0)),
111 Pose3::new(Isometry3::new(Quat::new(0.0, 0.0, 0.0, 1.0), Vec3::new(0.0, 0.0, 0.0))),
112 ))
113 .unwrap();
114 traj.push(StampedPose::new(
115 StampedTime::exact("host", ClockDomain::HostSteady, Timestamp::from_nanos(10)),
116 Pose3::new(Isometry3::new(Quat::new(0.0, 0.0, 0.0, 1.0), Vec3::new(10.0, 0.0, 0.0))),
117 ))
118 .unwrap();
119 let mid = traj.interpolate(5).unwrap();
120 assert!((mid.isometry.translation().x - 5.0).abs() < 1e-5);
121 }
122}