1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
use std::path::{Path, PathBuf};
use quick_xml::Reader;
use quick_xml::events::Event;
use super::error::ReadError;
/// A single timestep entry from a `.pvd` collection file.
#[derive(Clone, Debug)]
pub struct TimestepEntry {
/// Physical simulation time for this step.
pub time: f64,
/// Absolute path to the VTK data file for this step.
pub file: PathBuf,
/// Optional format-specific selector for timestep data within `file`.
///
/// Used by container formats like XDMF where multiple timesteps can live in a
/// single XML file.
pub selector: Option<String>,
}
/// A parsed PVD series containing all timestep entries, sorted by time.
#[derive(Clone, Debug)]
pub struct PvdSeries {
pub timesteps: Vec<TimestepEntry>,
}
/// Parse a `.pvd` XML collection file and return the series metadata.
///
/// File paths inside the PVD are resolved relative to the PVD file's parent directory.
/// The returned `timesteps` are sorted ascending by `time`.
#[allow(dead_code)]
pub fn read_pvd(path: &Path) -> Result<PvdSeries, ReadError> {
let parent = path.parent().unwrap_or(Path::new("."));
let file = std::fs::File::open(path)?;
let buf_reader = std::io::BufReader::new(file);
let mut reader = Reader::from_reader(buf_reader);
reader.config_mut().trim_text(true);
let mut timesteps: Vec<TimestepEntry> = Vec::new();
let mut buf = Vec::new();
loop {
match reader.read_event_into(&mut buf) {
Ok(Event::Empty(ref e)) | Ok(Event::Start(ref e)) => {
if e.local_name().as_ref() == b"DataSet" {
let mut time_val: Option<f64> = None;
let mut file_val: Option<String> = None;
for attr_result in e.attributes() {
let attr = attr_result
.map_err(|err| ReadError::Pvd(format!("XML attribute error: {err}")))?;
match attr.key.local_name().as_ref() {
b"timestep" => {
let v = attr.unescape_value().map_err(|err| {
ReadError::Pvd(format!("timestep unescape error: {err}"))
})?;
time_val = v.parse::<f64>().ok();
}
b"file" => {
let v = attr.unescape_value().map_err(|err| {
ReadError::Pvd(format!("file unescape error: {err}"))
})?;
file_val = Some(v.into_owned());
}
_ => {}
}
}
if let (Some(time), Some(rel_file)) = (time_val, file_val) {
let file_path = if Path::new(&rel_file).is_absolute() {
PathBuf::from(rel_file)
} else {
parent.join(&rel_file)
};
timesteps.push(TimestepEntry {
time,
file: file_path,
selector: None,
});
}
}
}
Ok(Event::Eof) => break,
Err(e) => {
return Err(ReadError::Pvd(format!("XML parse error: {e}")));
}
_ => {}
}
buf.clear();
}
if timesteps.is_empty() {
return Err(ReadError::Pvd(
"PVD file contains no DataSet entries".to_string(),
));
}
timesteps.sort_by(|a, b| {
a.time
.partial_cmp(&b.time)
.unwrap_or(std::cmp::Ordering::Equal)
});
Ok(PvdSeries { timesteps })
}