use arrow::array::{FixedSizeListArray, Int32Array, StructArray, UInt32Array};
use re_arrow_util::ArrowArrayDowncastRef as _;
use re_ros_msg::message_spec::BuiltInType;
use crate::parsers::ParserContext;
use super::decode_plan::{FieldLayout, MessageLayout, ValueLayout};
#[derive(Debug, strum::AsRefStr)]
#[strum(serialize_all = "kebab-case")]
pub(super) enum TimestampLocation {
None,
Header {
header: usize,
stamp: usize,
sec: usize,
nanosec: usize,
},
TopLevel {
stamp: usize,
sec: usize,
nanosec: usize,
},
}
impl TimestampLocation {
pub(super) fn from_messages(messages: &[MessageLayout], root: usize) -> Self {
if let Some((header_index, header)) = field_by_name(messages, root, "header") {
let ValueLayout::Message(header_id) = header.value() else {
return Self::None;
};
let Some((stamp, sec, nanosec)) = stamp_indexes(messages, *header_id) else {
return Self::None;
};
return Self::Header {
header: header_index,
stamp,
sec,
nanosec,
};
}
let Some((stamp, sec, nanosec)) = stamp_indexes(messages, root) else {
return Self::None;
};
Self::TopLevel {
stamp,
sec,
nanosec,
}
}
}
fn field_by_name<'a>(
messages: &'a [MessageLayout],
message_id: usize,
name: &str,
) -> Option<(usize, &'a FieldLayout)> {
messages[message_id]
.fields()
.iter()
.enumerate()
.find(|(_, field)| field.name() == name)
}
fn stamp_indexes(messages: &[MessageLayout], message_id: usize) -> Option<(usize, usize, usize)> {
let (stamp_index, stamp) = field_by_name(messages, message_id, "stamp")?;
let ValueLayout::Message(timestamp_id) = stamp.value() else {
return None;
};
let timestamp = &messages[*timestamp_id];
let sec = timestamp.fields().iter().position(|field| {
field.name() == "sec" && matches!(field.value(), ValueLayout::BuiltIn(BuiltInType::Int32))
})?;
let nanosec = timestamp.fields().iter().position(|field| {
field.name() == "nanosec"
&& matches!(field.value(), ValueLayout::BuiltIn(BuiltInType::UInt32))
})?;
Some((stamp_index, sec, nanosec))
}
pub(super) fn timestamp_columns<'a>(
location: &TimestampLocation,
root: &'a StructArray,
) -> Option<(&'a Int32Array, &'a UInt32Array)> {
let (timestamp_struct, sec_index, nanosec_index) = match location {
TimestampLocation::None => return None,
TimestampLocation::Header {
header,
stamp,
sec,
nanosec,
} => {
let header = root.column(*header).downcast_array_ref::<StructArray>()?;
let stamp = header.column(*stamp).downcast_array_ref::<StructArray>()?;
(stamp, *sec, *nanosec)
}
TimestampLocation::TopLevel {
stamp,
sec,
nanosec,
} => {
let stamp = root.column(*stamp).downcast_array_ref::<StructArray>()?;
(stamp, *sec, *nanosec)
}
};
let sec = timestamp_struct
.column(sec_index)
.downcast_array_ref::<Int32Array>()?;
let nanosec = timestamp_struct
.column(nanosec_index)
.downcast_array_ref::<UInt32Array>()?;
Some((sec, nanosec))
}
pub(super) fn timestamp_nanos(sec: i32, nanosec: u32) -> Option<u64> {
u64::try_from(i64::from(sec) * 1_000_000_000 + i64::from(nanosec)).ok()
}
pub(super) fn add_ros2_timestamps(
ctx: &mut ParserContext,
location: &TimestampLocation,
messages: &FixedSizeListArray,
) {
if matches!(location, TimestampLocation::None) {
return;
}
let columns = messages
.values()
.downcast_array_ref::<StructArray>()
.and_then(|root| timestamp_columns(location, root));
let Some((secs, nanosecs)) = columns else {
re_log::warn_once!(
"MCAP channel '{}' has a ROS 2 {} stamp that does not match its Arrow columns, so it will not appear on the `ros2_timestamp` timeline.",
ctx.channel_topic(),
location.as_ref()
);
return;
};
let nanos: Option<Vec<u64>> = std::iter::zip(secs.iter(), nanosecs.iter())
.map(|(sec, nanosec)| {
let (sec, nanosec) = Option::zip(sec, nanosec)?;
timestamp_nanos(sec, nanosec)
})
.collect();
let Some(nanos) = nanos else {
re_log::warn_once!(
"MCAP channel '{}' has a ROS 2 {} timestamp that cannot be represented, so it will not appear on the `ros2_timestamp` timeline.",
ctx.channel_topic(),
location.as_ref()
);
return;
};
let time_type = ctx.time_type();
for nanos in nanos {
ctx.add_timestamp_cell(crate::util::TimestampCell::from_nanos_ros2(
nanos, time_type,
));
}
}