1use bids_core::error::{BidsError, Result};
7use std::path::Path;
8
9#[derive(Debug, Clone, PartialEq)]
11pub struct DigPoint {
12 pub label: String,
14 pub x: f64,
16 pub y: f64,
18 pub z: f64,
20 pub kind: PointKind,
22}
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum PointKind {
27 Fiducial,
29 Hpi,
31 Eeg,
33 Extra,
35}
36
37pub fn read_headshape_pos(path: &Path) -> Result<Vec<DigPoint>> {
46 let contents = std::fs::read_to_string(path)?;
47 let mut points = Vec::new();
48
49 for (line_no, line) in contents.lines().enumerate() {
50 let line = line.trim();
51 if line.is_empty() || line.starts_with('#') || line.starts_with('%') {
52 continue;
53 }
54
55 let parts: Vec<&str> = line.split_whitespace().collect();
56
57 let (label, x, y, z) = if parts.len() >= 4 {
58 let x = parts[1]
60 .parse::<f64>()
61 .map_err(|_| parse_err(path, line_no))?;
62 let y = parts[2]
63 .parse::<f64>()
64 .map_err(|_| parse_err(path, line_no))?;
65 let z = parts[3]
66 .parse::<f64>()
67 .map_err(|_| parse_err(path, line_no))?;
68 (parts[0].to_string(), x, y, z)
69 } else if parts.len() == 3 {
70 let x = parts[0]
72 .parse::<f64>()
73 .map_err(|_| parse_err(path, line_no))?;
74 let y = parts[1]
75 .parse::<f64>()
76 .map_err(|_| parse_err(path, line_no))?;
77 let z = parts[2]
78 .parse::<f64>()
79 .map_err(|_| parse_err(path, line_no))?;
80 (format!("{}", points.len() + 1), x, y, z)
81 } else {
82 continue; };
84
85 let kind = classify_point(&label);
86 points.push(DigPoint {
87 label,
88 x,
89 y,
90 z,
91 kind,
92 });
93 }
94
95 Ok(points)
96}
97
98fn classify_point(label: &str) -> PointKind {
99 let lower = label.to_lowercase();
100 if lower == "nasion"
101 || lower == "nas"
102 || lower == "lpa"
103 || lower == "rpa"
104 || lower == "nz"
105 || lower == "left"
106 || lower == "right"
107 {
108 PointKind::Fiducial
109 } else if lower.starts_with("hpi") || lower.starts_with("coil") {
110 PointKind::Hpi
111 } else if lower.starts_with("eeg") || lower.starts_with("e") && lower.len() <= 4 {
112 PointKind::Eeg
113 } else {
114 PointKind::Extra
115 }
116}
117
118fn parse_err(path: &Path, line: usize) -> BidsError {
119 BidsError::DataFormat(format!(
120 "Cannot parse headshape file {} at line {}",
121 path.display(),
122 line + 1,
123 ))
124}
125
126#[must_use]
128pub fn count_by_kind(points: &[DigPoint]) -> (usize, usize, usize, usize) {
129 let mut fid = 0;
130 let mut hpi = 0;
131 let mut eeg = 0;
132 let mut extra = 0;
133 for p in points {
134 match p.kind {
135 PointKind::Fiducial => fid += 1,
136 PointKind::Hpi => hpi += 1,
137 PointKind::Eeg => eeg += 1,
138 PointKind::Extra => extra += 1,
139 }
140 }
141 (fid, hpi, eeg, extra)
142}
143
144#[cfg(test)]
145mod tests {
146 use super::*;
147 use std::io::Write;
148
149 #[test]
150 fn test_read_headshape_pos() {
151 let dir = std::env::temp_dir().join("bids_headshape_test");
152 std::fs::create_dir_all(&dir).unwrap();
153 let path = dir.join("test.pos");
154 let mut f = std::fs::File::create(&path).unwrap();
155 writeln!(f, "nasion 0.0 85.0 -40.0").unwrap();
156 writeln!(f, "lpa -80.0 0.0 -40.0").unwrap();
157 writeln!(f, "rpa 80.0 0.0 -40.0").unwrap();
158 writeln!(f, "1 10.0 20.0 30.0").unwrap();
159 writeln!(f, "2 11.0 21.0 31.0").unwrap();
160
161 let points = read_headshape_pos(&path).unwrap();
162 assert_eq!(points.len(), 5);
163 assert_eq!(points[0].kind, PointKind::Fiducial);
164 assert_eq!(points[0].label, "nasion");
165 assert!((points[0].y - 85.0).abs() < 1e-10);
166 assert_eq!(points[3].kind, PointKind::Extra);
167
168 let (fid, _, _, extra) = count_by_kind(&points);
169 assert_eq!(fid, 3);
170 assert_eq!(extra, 2);
171
172 std::fs::remove_dir_all(&dir).unwrap();
173 }
174
175 #[test]
176 fn test_read_no_labels() {
177 let dir = std::env::temp_dir().join("bids_headshape_nolabel");
178 std::fs::create_dir_all(&dir).unwrap();
179 let path = dir.join("test.pos");
180 let mut f = std::fs::File::create(&path).unwrap();
181 writeln!(f, "10.0 20.0 30.0").unwrap();
182 writeln!(f, "11.0 21.0 31.0").unwrap();
183
184 let points = read_headshape_pos(&path).unwrap();
185 assert_eq!(points.len(), 2);
186 assert_eq!(points[0].label, "1");
187
188 std::fs::remove_dir_all(&dir).unwrap();
189 }
190}