1use std::sync::{Arc, OnceLock};
4
5use arrow::array::{Array, ArrayRef, Float64Array, Int64Array, StringArray, StructArray};
6use arrow::datatypes::{DataType, Field, Schema, SchemaRef};
7use arrow::record_batch::RecordBatch;
8
9#[allow(missing_docs)]
11pub struct Odometry<'a> {
12 pub chassis_frame_id: &'a str,
13 pub odom_frame_id: &'a str,
14 pub position_x: f64,
15 pub position_y: f64,
16 pub position_z: f64,
17 pub orientation_w: f64,
18 pub orientation_x: f64,
19 pub orientation_y: f64,
20 pub orientation_z: f64,
21 pub lin_vel_x: f64,
22 pub lin_vel_y: f64,
23 pub lin_vel_z: f64,
24 pub ang_vel_x: f64,
25 pub ang_vel_y: f64,
26 pub ang_vel_z: f64,
27 pub timestamp_ns: i64,
28}
29
30#[derive(Debug, Clone, PartialEq)]
32#[allow(missing_docs)]
33pub struct OdometryOwned {
34 pub chassis_frame_id: String,
35 pub odom_frame_id: String,
36 pub position_x: f64,
37 pub position_y: f64,
38 pub position_z: f64,
39 pub orientation_w: f64,
40 pub orientation_x: f64,
41 pub orientation_y: f64,
42 pub orientation_z: f64,
43 pub lin_vel_x: f64,
44 pub lin_vel_y: f64,
45 pub lin_vel_z: f64,
46 pub ang_vel_x: f64,
47 pub ang_vel_y: f64,
48 pub ang_vel_z: f64,
49 pub timestamp_ns: i64,
50}
51
52pub fn schema() -> SchemaRef {
54 static SCHEMA: OnceLock<SchemaRef> = OnceLock::new();
55 SCHEMA
56 .get_or_init(|| {
57 Arc::new(Schema::new(vec![
58 Field::new("chassis_frame_id", DataType::Utf8, false),
59 Field::new("odom_frame_id", DataType::Utf8, false),
60 Field::new("position_x", DataType::Float64, false),
61 Field::new("position_y", DataType::Float64, false),
62 Field::new("position_z", DataType::Float64, false),
63 Field::new("orientation_w", DataType::Float64, false),
64 Field::new("orientation_x", DataType::Float64, false),
65 Field::new("orientation_y", DataType::Float64, false),
66 Field::new("orientation_z", DataType::Float64, false),
67 Field::new("lin_vel_x", DataType::Float64, false),
68 Field::new("lin_vel_y", DataType::Float64, false),
69 Field::new("lin_vel_z", DataType::Float64, false),
70 Field::new("ang_vel_x", DataType::Float64, false),
71 Field::new("ang_vel_y", DataType::Float64, false),
72 Field::new("ang_vel_z", DataType::Float64, false),
73 Field::new("timestamp_ns", DataType::Int64, false),
74 ]))
75 })
76 .clone()
77}
78
79pub fn to_record_batch(odom: &Odometry) -> Result<RecordBatch, arrow::error::ArrowError> {
99 let columns: Vec<ArrayRef> = vec![
100 Arc::new(StringArray::from(vec![odom.chassis_frame_id])),
101 Arc::new(StringArray::from(vec![odom.odom_frame_id])),
102 Arc::new(Float64Array::from_iter_values(std::iter::once(
103 odom.position_x,
104 ))),
105 Arc::new(Float64Array::from_iter_values(std::iter::once(
106 odom.position_y,
107 ))),
108 Arc::new(Float64Array::from_iter_values(std::iter::once(
109 odom.position_z,
110 ))),
111 Arc::new(Float64Array::from_iter_values(std::iter::once(
112 odom.orientation_w,
113 ))),
114 Arc::new(Float64Array::from_iter_values(std::iter::once(
115 odom.orientation_x,
116 ))),
117 Arc::new(Float64Array::from_iter_values(std::iter::once(
118 odom.orientation_y,
119 ))),
120 Arc::new(Float64Array::from_iter_values(std::iter::once(
121 odom.orientation_z,
122 ))),
123 Arc::new(Float64Array::from_iter_values(std::iter::once(
124 odom.lin_vel_x,
125 ))),
126 Arc::new(Float64Array::from_iter_values(std::iter::once(
127 odom.lin_vel_y,
128 ))),
129 Arc::new(Float64Array::from_iter_values(std::iter::once(
130 odom.lin_vel_z,
131 ))),
132 Arc::new(Float64Array::from_iter_values(std::iter::once(
133 odom.ang_vel_x,
134 ))),
135 Arc::new(Float64Array::from_iter_values(std::iter::once(
136 odom.ang_vel_y,
137 ))),
138 Arc::new(Float64Array::from_iter_values(std::iter::once(
139 odom.ang_vel_z,
140 ))),
141 Arc::new(Int64Array::from_iter_values(std::iter::once(
142 odom.timestamp_ns,
143 ))),
144 ];
145 RecordBatch::try_new(schema(), columns)
146}
147
148pub fn from_struct_array(array: &StructArray) -> Result<OdometryOwned, arrow::error::ArrowError> {
171 if array.is_empty() {
172 return Err(arrow::error::ArrowError::InvalidArgumentError(
173 "odometry struct array is empty".into(),
174 ));
175 }
176 let str_at = |idx: usize, name: &str| -> Result<String, arrow::error::ArrowError> {
177 array
178 .column(idx)
179 .as_any()
180 .downcast_ref::<StringArray>()
181 .ok_or_else(|| {
182 arrow::error::ArrowError::SchemaError(format!("odometry '{name}' not Utf8"))
183 })
184 .map(|a| a.value(0).to_string())
185 };
186 let f64_at = |idx: usize, name: &str| -> Result<f64, arrow::error::ArrowError> {
187 array
188 .column(idx)
189 .as_any()
190 .downcast_ref::<Float64Array>()
191 .ok_or_else(|| {
192 arrow::error::ArrowError::SchemaError(format!("odometry '{name}' not Float64"))
193 })
194 .map(|a| a.value(0))
195 };
196 Ok(OdometryOwned {
197 chassis_frame_id: str_at(0, "chassis_frame_id")?,
198 odom_frame_id: str_at(1, "odom_frame_id")?,
199 position_x: f64_at(2, "position_x")?,
200 position_y: f64_at(3, "position_y")?,
201 position_z: f64_at(4, "position_z")?,
202 orientation_w: f64_at(5, "orientation_w")?,
203 orientation_x: f64_at(6, "orientation_x")?,
204 orientation_y: f64_at(7, "orientation_y")?,
205 orientation_z: f64_at(8, "orientation_z")?,
206 lin_vel_x: f64_at(9, "lin_vel_x")?,
207 lin_vel_y: f64_at(10, "lin_vel_y")?,
208 lin_vel_z: f64_at(11, "lin_vel_z")?,
209 ang_vel_x: f64_at(12, "ang_vel_x")?,
210 ang_vel_y: f64_at(13, "ang_vel_y")?,
211 ang_vel_z: f64_at(14, "ang_vel_z")?,
212 timestamp_ns: array
213 .column(15)
214 .as_any()
215 .downcast_ref::<Int64Array>()
216 .ok_or_else(|| {
217 arrow::error::ArrowError::SchemaError("odometry 'timestamp_ns' not Int64".into())
218 })?
219 .value(0),
220 })
221}
222
223#[cfg(test)]
224mod tests {
225 use super::*;
226 use arrow::array::Array;
227
228 #[test]
229 fn round_trips_through_record_batch() {
230 let odom = Odometry {
231 chassis_frame_id: "base_link",
232 odom_frame_id: "odom",
233 position_x: 1.0,
234 position_y: 2.0,
235 position_z: 0.0,
236 orientation_w: 1.0,
237 orientation_x: 0.0,
238 orientation_y: 0.0,
239 orientation_z: 0.0,
240 lin_vel_x: 0.4,
241 lin_vel_y: 0.0,
242 lin_vel_z: 0.0,
243 ang_vel_x: 0.0,
244 ang_vel_y: 0.0,
245 ang_vel_z: 0.3,
246 timestamp_ns: 7,
247 };
248 let batch = to_record_batch(&odom).expect("convert");
249 assert_eq!(batch.num_rows(), 1);
250 assert_eq!(batch.num_columns(), 16);
251
252 let chassis = batch
253 .column(0)
254 .as_any()
255 .downcast_ref::<StringArray>()
256 .expect("chassis_frame_id is Utf8");
257 assert_eq!(chassis.value(0), "base_link");
258
259 let lin_x = batch
260 .column(9)
261 .as_any()
262 .downcast_ref::<Float64Array>()
263 .expect("lin_vel_x is Float64");
264 assert!((lin_x.value(0) - 0.4).abs() < 1e-9);
265 }
266
267 #[test]
268 fn from_struct_array_round_trips() {
269 let odom = Odometry {
270 chassis_frame_id: "base_link",
271 odom_frame_id: "odom",
272 position_x: 1.0,
273 position_y: 2.0,
274 position_z: 0.0,
275 orientation_w: 1.0,
276 orientation_x: 0.0,
277 orientation_y: 0.0,
278 orientation_z: 0.0,
279 lin_vel_x: 0.4,
280 lin_vel_y: 0.0,
281 lin_vel_z: 0.0,
282 ang_vel_x: 0.0,
283 ang_vel_y: 0.0,
284 ang_vel_z: 0.3,
285 timestamp_ns: 7,
286 };
287 let batch = to_record_batch(&odom).expect("to");
288 let array = StructArray::from(batch);
289 let owned = from_struct_array(&array).expect("from");
290 assert_eq!(owned.chassis_frame_id, "base_link");
291 assert_eq!(owned.odom_frame_id, "odom");
292 assert!((owned.lin_vel_x - 0.4).abs() < 1e-9);
293 assert_eq!(owned.timestamp_ns, 7);
294 }
295}