use crate::{Channel, Mf4Error, Mf4File, Result, SignalValues};
pub fn element_values(
values: &SignalValues,
validity: Option<&[bool]>,
element: usize,
) -> Result<Vec<f64>> {
let flat = values.to_f64();
let n = values.len();
let mut out = Vec::with_capacity(n);
for i in 0..n {
let index = match values {
SignalValues::Array {
elements_per_sample,
..
} => {
if element >= *elements_per_sample {
return Err(Mf4Error::parse_error("array element out of range"));
}
Some(i * elements_per_sample + element)
}
SignalValues::ArrayVarLen { starts, .. } => {
let a = starts[i];
let b = starts[i + 1];
(element < b - a).then_some(a + element)
}
_ if element == 0 => Some(i),
_ => {
return Err(Mf4Error::parse_error(
"scalar channel has only element zero",
))
}
};
let valid = index.is_some_and(|j| {
validity.is_none_or(|v| {
if v.len() == n {
v.get(i).copied().unwrap_or(false)
} else {
v.get(j).copied().unwrap_or(false)
}
})
});
out.push(if valid {
index.and_then(|j| flat.get(j)).copied().unwrap_or(f64::NAN)
} else {
f64::NAN
});
}
Ok(out)
}
#[derive(Debug, Default)]
pub struct ViewWindow {
pub timestamps: Vec<f64>,
pub values: Vec<f64>,
pub extent: Option<(f64, f64)>,
pub total: usize,
}
#[derive(Clone, Copy)]
struct Point {
index: usize,
time: f64,
value: f64,
}
#[derive(Default)]
struct Bucket {
first: Option<Point>,
last: Option<Point>,
min: Option<Point>,
max: Option<Point>,
gap: Option<Point>,
}
impl Bucket {
fn push(&mut self, point: Point) {
self.first.get_or_insert(point);
self.last = Some(point);
if point.value.is_finite() {
if self.min.is_none_or(|p| point.value < p.value) {
self.min = Some(point);
}
if self.max.is_none_or(|p| point.value > p.value) {
self.max = Some(point);
}
} else {
self.gap.get_or_insert(point);
}
}
}
impl Mf4File {
pub fn visit_samples<F>(
&self,
channel: &Channel,
validate_axis: bool,
mut visit: F,
) -> Result<()>
where
F: FnMut(usize, &[f64], &SignalValues, Option<&[bool]>) -> Result<bool>,
{
let master = self.master_channel(channel.data_group_index, channel.channel_group_index);
let mut selected = vec![channel];
if let Some(master) = master {
selected.push(master);
}
let mut chunks = self.signals_chunks(&selected, 16_384)?;
let mut offset = 0;
let mut previous = None;
for chunk in &mut chunks {
let chunk = chunk?;
let signal = &chunk[0];
let mut times = if let Some(master) = master {
let sig = &chunk[1];
let mut times = sig.values_f64()?;
let validity = sig.validity();
if validate_axis {
crate::time_ops::validate_master_axis(
&master.name,
×,
validity.as_deref(),
offset,
previous,
)?;
} else if let Some(validity) = validity {
for (t, valid) in times.iter_mut().zip(validity) {
if !valid {
*t = f64::NAN;
}
}
}
times
} else {
(offset..offset + signal.len()).map(|i| i as f64).collect()
};
if times.len() != signal.len() {
return Err(Mf4Error::parse_error("master/sample length mismatch"));
}
previous = times.last().copied().or(previous);
if !visit(
offset,
×,
&signal.values()?,
signal.validity().as_deref(),
)? {
break;
}
offset += times.len();
times.clear();
}
Ok(())
}
pub fn view_window(
&self,
channel: &Channel,
element: usize,
t0: f64,
t1: f64,
max_points: usize,
) -> Result<ViewWindow> {
if !(5..=1_000_000).contains(&max_points) {
return Err(Mf4Error::parse_error(
"point budget must be between 5 and 1000000",
));
}
let mut result = ViewWindow::default();
self.visit_samples(channel, true, |_, times, _, _| {
if let (Some(&a), Some(&b)) = (times.first(), times.last()) {
result.extent = Some((result.extent.map_or(a, |e| e.0), b));
}
result.total += times.len();
Ok(true)
})?;
let Some((a, b)) = result.extent else {
return Ok(result);
};
if t0.is_nan() || t1.is_nan() || t0 > t1 {
return Ok(result);
}
let lo = if t0.is_finite() { t0.max(a) } else { a };
let hi = if t1.is_finite() { t1.min(b) } else { b };
if lo > hi {
return Ok(result);
}
let columns = max_points / 5;
let mut buckets: Vec<Bucket> = (0..columns).map(|_| Bucket::default()).collect();
self.visit_samples(channel, true, |offset, times, values, validity| {
let values = element_values(values, validity, element)?;
for (i, (&time, &value)) in times.iter().zip(&values).enumerate() {
if time < lo || time > hi {
continue;
}
let column = if hi > lo {
(((time - lo) / (hi - lo)) * columns as f64) as usize
} else {
0
}
.min(columns - 1);
buckets[column].push(Point {
index: offset + i,
time,
value,
});
}
Ok(true)
})?;
for bucket in buckets {
let mut points: Vec<Point> = [
bucket.first,
bucket.min,
bucket.max,
bucket.gap,
bucket.last,
]
.into_iter()
.flatten()
.collect();
points.sort_by_key(|p| p.index);
points.dedup_by_key(|p| p.index);
for point in points {
result.timestamps.push(point.time);
result.values.push(point.value);
}
}
Ok(result)
}
}
#[derive(Debug, Default)]
pub struct TextWindow {
pub timestamps: Vec<f64>,
pub labels: Vec<Option<String>>,
pub extent: Option<(f64, f64)>,
pub truncated: bool,
}
impl Mf4File {
pub fn view_text_window(
&self,
channel: &Channel,
t0: f64,
t1: f64,
max_points: usize,
) -> Result<TextWindow> {
if !(2..=1_000_000).contains(&max_points) {
return Err(Mf4Error::parse_error(
"text point budget must be between 2 and 1000000",
));
}
let mut result = TextWindow::default();
let mut transitions = 0usize;
let mut previous: Option<Option<String>> = None;
self.visit_samples(channel, true, |_, times, values, validity| {
let SignalValues::Str(labels) = values else {
return Err(Mf4Error::parse_error("text window requires a text channel"));
};
if let (Some(&a), Some(&b)) = (times.first(), times.last()) {
result.extent = Some((result.extent.map_or(a, |e| e.0), b));
}
for (i, (&time, label)) in times.iter().zip(labels).enumerate() {
if time < t0 || time > t1 || t0.is_nan() || t1.is_nan() {
continue;
}
let label = validity.is_none_or(|v| v[i]).then(|| label.clone());
if previous.as_ref() != Some(&label) {
transitions += 1;
previous = Some(label);
}
}
Ok(true)
})?;
let stride = transitions
.div_ceil(max_points.saturating_sub(1).max(1))
.max(1);
result.truncated = transitions + 1 > max_points;
previous = None;
let mut transition = 0usize;
let mut last = None;
self.visit_samples(channel, true, |_, times, values, validity| {
let SignalValues::Str(labels) = values else {
return Err(Mf4Error::parse_error("text window requires a text channel"));
};
for (i, (&time, label)) in times.iter().zip(labels).enumerate() {
if time < t0 || time > t1 || t0.is_nan() || t1.is_nan() {
continue;
}
let label = validity.is_none_or(|v| v[i]).then(|| label.clone());
if previous.as_ref() != Some(&label) {
if transition.is_multiple_of(stride) {
result.timestamps.push(time);
result.labels.push(label.clone());
}
transition += 1;
previous = Some(label.clone());
}
last = Some((time, label));
}
Ok(true)
})?;
if let Some((time, label)) = last {
if result.timestamps.last() != Some(&time) || result.labels.last() != Some(&label) {
result.timestamps.push(time);
result.labels.push(label);
}
}
Ok(result)
}
}