1use std::sync::{Arc, OnceLock};
4
5use arrow::array::{Array, ArrayRef, Float32Array, Int32Array, ListArray, StructArray};
6use arrow::buffer::OffsetBuffer;
7use arrow::datatypes::{DataType, Field, Schema, SchemaRef};
8use arrow::record_batch::RecordBatch;
9
10#[allow(missing_docs)]
12pub struct LidarPointCloud<'a> {
13 pub points: &'a [f32],
14 pub num_points: i32,
15 pub width: i32,
16 pub height: i32,
17}
18
19#[derive(Debug, Clone, PartialEq)]
22#[allow(missing_docs)]
23pub struct LidarPointCloudOwned {
24 pub points: Vec<f32>,
25 pub num_points: i32,
26 pub width: i32,
27 pub height: i32,
28}
29
30pub fn schema() -> SchemaRef {
32 static SCHEMA: OnceLock<SchemaRef> = OnceLock::new();
33 SCHEMA
34 .get_or_init(|| {
35 Arc::new(Schema::new(vec![
36 Field::new(
37 "points",
38 DataType::List(Arc::new(Field::new("item", DataType::Float32, false))),
39 false,
40 ),
41 Field::new("num_points", DataType::Int32, false),
42 Field::new("width", DataType::Int32, false),
43 Field::new("height", DataType::Int32, false),
44 ]))
45 })
46 .clone()
47}
48
49fn list_f32(values: &[f32]) -> ListArray {
50 let inner = Float32Array::from_iter_values(values.iter().copied());
51 let offsets = OffsetBuffer::from_lengths([values.len()]);
52 ListArray::new(
53 Arc::new(Field::new("item", DataType::Float32, false)),
54 offsets,
55 Arc::new(inner),
56 None,
57 )
58}
59
60pub fn to_record_batch(pc: &LidarPointCloud) -> Result<RecordBatch, arrow::error::ArrowError> {
74 let columns: Vec<ArrayRef> = vec![
75 Arc::new(list_f32(pc.points)),
76 Arc::new(Int32Array::from_iter_values(std::iter::once(pc.num_points))),
77 Arc::new(Int32Array::from_iter_values(std::iter::once(pc.width))),
78 Arc::new(Int32Array::from_iter_values(std::iter::once(pc.height))),
79 ];
80 RecordBatch::try_new(schema(), columns)
81}
82
83#[allow(missing_docs)]
86pub struct LidarPointCloudBorrowed<'a> {
87 pub points: &'a [f32],
88 pub num_points: i32,
89 pub width: i32,
90 pub height: i32,
91}
92
93pub fn from_struct_array_borrowed(
96 array: &StructArray,
97) -> Result<LidarPointCloudBorrowed<'_>, arrow::error::ArrowError> {
98 if array.is_empty() {
99 return Err(arrow::error::ArrowError::InvalidArgumentError(
100 "lidar_pointcloud struct array is empty".into(),
101 ));
102 }
103 let pts_list = array
104 .column(0)
105 .as_any()
106 .downcast_ref::<ListArray>()
107 .ok_or_else(|| {
108 arrow::error::ArrowError::SchemaError("pointcloud 'points' not ListArray".into())
109 })?;
110 let points = pts_list
111 .values()
112 .as_any()
113 .downcast_ref::<Float32Array>()
114 .ok_or_else(|| {
115 arrow::error::ArrowError::SchemaError("pointcloud 'points' inner not Float32".into())
116 })?
117 .values();
118 let i32_at = |idx: usize, name: &str| -> Result<i32, arrow::error::ArrowError> {
119 array
120 .column(idx)
121 .as_any()
122 .downcast_ref::<Int32Array>()
123 .ok_or_else(|| {
124 arrow::error::ArrowError::SchemaError(format!("pointcloud '{name}' not Int32"))
125 })
126 .map(|a| a.value(0))
127 };
128 Ok(LidarPointCloudBorrowed {
129 points,
130 num_points: i32_at(1, "num_points")?,
131 width: i32_at(2, "width")?,
132 height: i32_at(3, "height")?,
133 })
134}
135
136pub fn from_struct_array(
152 array: &StructArray,
153) -> Result<LidarPointCloudOwned, arrow::error::ArrowError> {
154 if array.is_empty() {
155 return Err(arrow::error::ArrowError::InvalidArgumentError(
156 "lidar_pointcloud struct array is empty".into(),
157 ));
158 }
159 let pts_list = array
160 .column(0)
161 .as_any()
162 .downcast_ref::<ListArray>()
163 .ok_or_else(|| {
164 arrow::error::ArrowError::SchemaError("pointcloud 'points' not ListArray".into())
165 })?;
166 let pts_inner = pts_list
167 .values()
168 .as_any()
169 .downcast_ref::<Float32Array>()
170 .ok_or_else(|| {
171 arrow::error::ArrowError::SchemaError("pointcloud 'points' inner not Float32".into())
172 })?;
173 let i32_at = |idx: usize, name: &str| -> Result<i32, arrow::error::ArrowError> {
174 array
175 .column(idx)
176 .as_any()
177 .downcast_ref::<Int32Array>()
178 .ok_or_else(|| {
179 arrow::error::ArrowError::SchemaError(format!("pointcloud '{name}' not Int32"))
180 })
181 .map(|a| a.value(0))
182 };
183 Ok(LidarPointCloudOwned {
184 points: pts_inner.values().to_vec(),
185 num_points: i32_at(1, "num_points")?,
186 width: i32_at(2, "width")?,
187 height: i32_at(3, "height")?,
188 })
189}
190
191#[cfg(test)]
192mod tests {
193 use super::*;
194
195 #[test]
196 fn round_trips_through_record_batch() {
197 let points = [
198 1.0_f32, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0,
201 ];
202 let pc = LidarPointCloud {
203 points: &points,
204 num_points: 3,
205 width: 3,
206 height: 1,
207 };
208 let batch = to_record_batch(&pc).expect("convert");
209 assert_eq!(batch.num_rows(), 1);
210 assert_eq!(batch.num_columns(), 4);
211
212 let pts_col = batch
213 .column(0)
214 .as_any()
215 .downcast_ref::<ListArray>()
216 .expect("points is ListArray");
217 let inner = pts_col
218 .values()
219 .as_any()
220 .downcast_ref::<Float32Array>()
221 .expect("inner is Float32Array");
222 assert_eq!(inner.len(), 9);
223 assert_eq!(inner.value(0), 1.0);
224 assert_eq!(inner.value(8), 1.0);
225 }
226
227 #[test]
228 fn from_struct_array_round_trips() {
229 let points = [1.0_f32, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0];
230 let pc = LidarPointCloud {
231 points: &points,
232 num_points: 3,
233 width: 3,
234 height: 1,
235 };
236 let batch = to_record_batch(&pc).expect("to");
237 let array = StructArray::from(batch);
238 let owned = from_struct_array(&array).expect("from");
239 assert_eq!(owned.points, points);
240 assert_eq!(owned.num_points, 3);
241 }
242}