use std::sync::Arc;
use crate::error::{Mf4Error, Result};
use crate::file::Mf4File;
use crate::model::{Channel, SignalValues};
use crate::time_ops::SignalSeries;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ChannelSelector {
Name(String),
NameInGroup {
name: String,
data_group: usize,
channel_group: usize,
},
Position {
data_group: usize,
channel_group: usize,
index: usize,
},
}
impl From<&str> for ChannelSelector {
fn from(name: &str) -> Self {
ChannelSelector::Name(name.to_string())
}
}
impl From<String> for ChannelSelector {
fn from(name: String) -> Self {
ChannelSelector::Name(name)
}
}
impl ChannelSelector {
pub(crate) fn resolve<'a>(&self, file: &'a Mf4File) -> Result<&'a Channel> {
match self {
ChannelSelector::Name(name) => {
let found = file.find_channels(name);
match found.len() {
0 => Err(Mf4Error::ChannelNotFound { name: name.clone() }),
1 => Ok(found[0]),
n => Err(Mf4Error::parse_error(format!(
"channel name '{name}' is carried by {n} channels; select it with \
ChannelSelector::NameInGroup or ChannelSelector::Position"
))),
}
}
ChannelSelector::NameInGroup {
name,
data_group,
channel_group,
} => {
let group = group_at(file, *data_group, *channel_group)?;
group.find_channel(name).ok_or_else(|| {
Mf4Error::parse_error(format!(
"channel '{name}' not found in data group {data_group}, channel group \
{channel_group}"
))
})
}
ChannelSelector::Position {
data_group,
channel_group,
index,
} => {
let group = group_at(file, *data_group, *channel_group)?;
group.channels.get(*index).ok_or_else(|| {
Mf4Error::parse_error(format!(
"channel index {index} is out of range for data group {data_group}, \
channel group {channel_group}, which has {} channels",
group.channels.len()
))
})
}
}
}
}
fn group_at(
file: &Mf4File,
data_group: usize,
channel_group: usize,
) -> Result<&crate::model::ChannelGroup> {
let dg = file.data_groups().get(data_group).ok_or_else(|| {
Mf4Error::parse_error(format!(
"data group {data_group} is out of range; the file has {}",
file.data_groups().len()
))
})?;
dg.channel_groups.get(channel_group).ok_or_else(|| {
Mf4Error::parse_error(format!(
"channel group {channel_group} is out of range for data group {data_group}, which \
has {}",
dg.channel_groups.len()
))
})
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum TimeAlignment {
#[default]
StartTime,
AsRecorded,
}
#[derive(Debug, Clone, PartialEq)]
pub struct StackedSeries {
pub file_index: usize,
pub series: SignalSeries,
}
pub fn concatenate(files: &[&Mf4File], alignment: TimeAlignment) -> Result<Vec<SignalSeries>> {
if files.is_empty() {
return Err(Mf4Error::parse_error("concatenate needs at least one file"));
}
let offsets = start_time_offsets(files, alignment);
let layouts: Vec<Vec<GroupLayout<'_>>> = files.iter().map(|f| group_layouts(f)).collect();
check_same_structure(&layouts)?;
let mut out: Vec<SignalSeries> = Vec::new();
for (group_index, first_group) in layouts[0].iter().enumerate() {
if first_group.channels.is_empty() {
continue;
}
let mut timestamps: Vec<Vec<f64>> = vec![Vec::new(); first_group.channels.len()];
let mut values: Vec<Option<SignalValues>> = vec![None; first_group.channels.len()];
let mut validity: Vec<Option<Vec<bool>>> = vec![None; first_group.channels.len()];
let mut counts: Vec<usize> = vec![0; first_group.channels.len()];
let mut last_timestamp: Option<f64> = None;
for (file_index, file) in files.iter().enumerate() {
let group = &layouts[file_index][group_index];
let mut taken = vec![false; group.channels.len()];
let selectors: Vec<&Channel> = first_group
.channels
.iter()
.map(|ch| {
let at = group
.channels
.iter()
.enumerate()
.find(|(i, other)| other.name == ch.name && !taken[*i])
.map(|(i, _)| i)
.expect("the structure check matched the names as multisets");
taken[at] = true;
group.channels[at]
})
.collect();
let series = file.series_for(&selectors)?;
let master: Vec<f64> = match series.first() {
Some(first) if !first.timestamps.is_empty() => {
let mut master: Vec<f64> = first
.timestamps
.iter()
.map(|t| t + offsets[file_index])
.collect();
if let Some(last) = last_timestamp {
if last >= master[0] {
let delta = if master.len() >= 2 {
master[1] - master[0]
} else {
0.001
};
let shift = last + delta - master[0];
for t in &mut master {
*t += shift;
}
}
}
last_timestamp = master.last().copied();
master
}
_ => Vec::new(),
};
for (slot, s) in series.into_iter().enumerate() {
timestamps[slot].extend_from_slice(&master);
let added = s.values.len();
append_validity(
&mut validity[slot],
counts[slot],
s.validity.as_deref(),
added,
);
counts[slot] += added;
match &mut values[slot] {
Some(acc) => append_values(acc, &s.values)?,
none => *none = Some(s.values),
}
}
}
for (slot, channel) in first_group.channels.iter().enumerate() {
let vals = values[slot].take().unwrap_or(SignalValues::F64(Vec::new()));
out.push(SignalSeries::new(
(*channel).clone(),
std::mem::take(&mut timestamps[slot]),
vals,
validity[slot].take(),
)?);
}
}
Ok(out)
}
pub fn stack(files: &[&Mf4File], alignment: TimeAlignment) -> Result<Vec<StackedSeries>> {
if files.is_empty() {
return Err(Mf4Error::parse_error("stack needs at least one file"));
}
let offsets = start_time_offsets(files, alignment);
let mut out = Vec::new();
for (file_index, file) in files.iter().enumerate() {
let offset = offsets[file_index];
for group in group_layouts(file) {
if group.channels.is_empty() {
continue;
}
let series_list = file.series_for(&group.channels)?;
let shifted = series_list.first().map(|first| {
Arc::new(
first
.timestamps()
.iter()
.map(|t| t + offset)
.collect::<Vec<f64>>(),
)
});
for mut series in series_list {
if let Some(shifted) = &shifted {
if shifted.len() == series.timestamps.len() {
series.timestamps = Arc::clone(shifted);
out.push(StackedSeries { file_index, series });
continue;
}
}
for t in Arc::make_mut(&mut series.timestamps) {
*t += offset;
}
out.push(StackedSeries { file_index, series });
}
}
}
Ok(out)
}
struct GroupLayout<'a> {
channels: Vec<&'a Channel>,
}
fn group_layouts(file: &Mf4File) -> Vec<GroupLayout<'_>> {
file.data_groups()
.iter()
.flat_map(|dg| dg.channel_groups.iter())
.map(|cg| GroupLayout {
channels: cg.channels.iter().filter(|ch| !ch.is_master()).collect(),
})
.collect()
}
fn start_time_offsets(files: &[&Mf4File], alignment: TimeAlignment) -> Vec<f64> {
match alignment {
TimeAlignment::AsRecorded => vec![0.0; files.len()],
TimeAlignment::StartTime => {
let starts: Vec<i64> = files.iter().map(|f| f.start_time().timestamp_ns).collect();
let oldest = starts.iter().copied().min().unwrap_or(0);
starts
.iter()
.map(|&ns| (ns.saturating_sub(oldest) as f64 / 1e9).max(0.0))
.collect()
}
}
}
fn check_same_structure(layouts: &[Vec<GroupLayout<'_>>]) -> Result<()> {
let first = &layouts[0];
for (file_index, layout) in layouts.iter().enumerate().skip(1) {
if layout.len() != first.len() {
return Err(Mf4Error::parse_error(format!(
"cannot concatenate: file 0 has {} channel groups but file {file_index} has {}",
first.len(),
layout.len()
)));
}
for (group_index, (a, b)) in first.iter().zip(layout).enumerate() {
let mut want: Vec<&str> = a.channels.iter().map(|ch| ch.name.as_str()).collect();
let mut got: Vec<&str> = b.channels.iter().map(|ch| ch.name.as_str()).collect();
want.sort_unstable();
got.sort_unstable();
if want != got {
return Err(Mf4Error::parse_error(format!(
"cannot concatenate: channel group {group_index} holds {want:?} in file 0 \
but {got:?} in file {file_index}"
)));
}
}
}
Ok(())
}
fn append_validity(
acc: &mut Option<Vec<bool>>,
acc_len: usize,
next: Option<&[bool]>,
added: usize,
) {
match (acc.as_mut(), next) {
(None, None) => {}
(None, Some(v)) => {
let mut mask = vec![true; acc_len];
mask.extend_from_slice(v);
*acc = Some(mask);
}
(Some(mask), Some(v)) => mask.extend_from_slice(v),
(Some(mask), None) => mask.extend(std::iter::repeat_n(true, added)),
}
}
fn append_values(acc: &mut SignalValues, next: &SignalValues) -> Result<()> {
fn mismatch(acc: &SignalValues, next: &SignalValues) -> Mf4Error {
Mf4Error::parse_error(format!(
"cannot concatenate {:?} samples onto {:?} samples",
next.kind(),
acc.kind()
))
}
match (acc, next) {
(SignalValues::U8(a), SignalValues::U8(b)) => a.extend_from_slice(b),
(SignalValues::U16(a), SignalValues::U16(b)) => a.extend_from_slice(b),
(SignalValues::U32(a), SignalValues::U32(b)) => a.extend_from_slice(b),
(SignalValues::U64(a), SignalValues::U64(b)) => a.extend_from_slice(b),
(SignalValues::I8(a), SignalValues::I8(b)) => a.extend_from_slice(b),
(SignalValues::I16(a), SignalValues::I16(b)) => a.extend_from_slice(b),
(SignalValues::I32(a), SignalValues::I32(b)) => a.extend_from_slice(b),
(SignalValues::I64(a), SignalValues::I64(b)) => a.extend_from_slice(b),
(SignalValues::F32(a), SignalValues::F32(b)) => a.extend_from_slice(b),
(SignalValues::F64(a), SignalValues::F64(b)) => a.extend_from_slice(b),
(SignalValues::Str(a), SignalValues::Str(b)) => a.extend_from_slice(b),
(SignalValues::CanopenDate(a), SignalValues::CanopenDate(b)) => a.extend_from_slice(b),
(SignalValues::CanopenTime(a), SignalValues::CanopenTime(b)) => a.extend_from_slice(b),
(SignalValues::Complex { re, im }, SignalValues::Complex { re: re_b, im: im_b }) => {
re.extend_from_slice(re_b);
im.extend_from_slice(im_b);
}
(
SignalValues::Bytes { data, width },
SignalValues::Bytes {
data: data_b,
width: width_b,
},
) => {
if width != width_b {
return Err(Mf4Error::parse_error(format!(
"cannot concatenate {width_b}-byte samples onto {width}-byte samples"
)));
}
data.extend_from_slice(data_b);
}
(
SignalValues::VarBytes { data, starts },
SignalValues::VarBytes {
data: data_b,
starts: starts_b,
},
) => {
let base = data.len();
data.extend_from_slice(data_b);
starts.extend(starts_b.iter().skip(1).map(|&s| s + base));
}
(
SignalValues::Array {
values,
elements_per_sample,
},
SignalValues::Array {
values: values_b,
elements_per_sample: per_b,
},
) => {
if elements_per_sample != per_b {
return Err(Mf4Error::parse_error(format!(
"cannot concatenate {per_b}-element samples onto \
{elements_per_sample}-element samples"
)));
}
values.extend_from_slice(values_b);
}
(
SignalValues::ArrayVarLen { values, starts },
SignalValues::ArrayVarLen {
values: values_b,
starts: starts_b,
},
) => {
let base = values.len();
values.extend_from_slice(values_b);
starts.extend(starts_b.iter().skip(1).map(|&s| s + base));
}
(acc, next) => return Err(mismatch(acc, next)),
}
Ok(())
}